← Back to Writeups
HTBN/ASteganography

Take a Slice

XESXOR8/23/20266 min read
#steganography#htb#n/a

Take a Slice

Platform: Umasscybersec | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-11 | Status: Solved Techniques: binary_stl_parsing, connected_component_isolation, pca_projection, projected_triangle_rendering

Summary

Task: a file named cake looked like opaque binary data, but its structure matched a binary STL 3D mesh with hidden geometry. Solution: parse triangles, isolate disconnected mesh components, project the small hidden meshes with PCA, and render filled projected triangles to read UMASS{REDACTED}.

Recon

Port scan

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

Enumeration highlights

  • Event: umasscybersec | ID: 20260411_umasscybersec_take_a_slice
  • Tags: pca, stl, binary_stl, mesh_steganography, hidden_geometry, 3d_model
  • Indicators: A file identified only as generic data but matching the binary STL layout, Binary STL header with a plausible triangle count at offset 80, Raw top/front/side projections showing faint letter-like artifacts, Many disconnected mesh components where one large model coexists with several tiny ones, Hidden content becomes readable only after projecting isolated geometry and filling triangles
  • Source: 20260411_umasscybersec_take_a_slice.md

Foothold

Vulnerability / Misconfiguration

  1. Binary_stl_parsing
  2. Connected_component_isolation
  3. Pca_projection
  4. Projected_triangle_rendering
<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

  • binary_stl_parsing
  • connected_component_isolation
  • pca_projection
  • projected_triangle_rendering
  • Tags: pca, stl, binary_stl, mesh_steganography, hidden_geometry, 3d_model

Original Writeup

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

Challenge

It's in the name!

The challenge provided a single file named cake. It was not immediately recognized as a common media format, so the goal was to identify the container first and then determine where the hidden data was actually stored.

Recon

Basic triage did not reveal anything obvious:

file cake
# data

That ruled out easy wins like plain text, images, archives, or obvious appended content. A quick hex look was more useful: the file had a mostly zero 80-byte header followed by data that looked structured rather than random.

That pattern strongly suggested a binary STL file:

  • 80-byte header
  • 4-byte little-endian triangle count
  • then 50 bytes per triangle

Parsing offset 80 as a little-endian uint32 gave a triangle count of 39210, which is exactly what a binary STL stores after the header. The geometry bounds were approximately:

  • x: [-1.347, 59.055]
  • y: [-2.54, 43.105]
  • z: [0, 25.4]

So the mystery cake file was really a 3D model.

The binary STL record layout is:

  • 12 bytes: normal vector (float32 x 3)
  • 36 bytes: 3 vertices (float32 x 9)
  • 2 bytes: attribute field

No useful strings or metadata were present. The flag was hidden in the mesh itself.

Analysis

At this point, the main question was whether the model geometry itself encoded something visual. There were no relevant STL-specific hits in the existing knowledge base or HackTricks, so the solve path came from direct geometry analysis.

Plotting raw projections of all triangles from the top, front, and side views produced suspicious artifacts. They were not fully readable, but they looked too structured to be accidental. The helper renders in the task directory captured this stage:

  • tasks/umasscybersec/Take a Slice/top.png
  • tasks/umasscybersec/Take a Slice/front.png
  • tasks/umasscybersec/Take a Slice/side.png

That suggested the text was not hidden in metadata or bit-level encoding, but as separate geometry embedded inside the STL.

The key insight was to treat the mesh as a graph:

  • each unique vertex is a node
  • triangles connect their three vertices
  • connected triangles belong to the same mesh component

Running connected-component analysis over shared vertices found:

  • 20 total mesh components
  • 1 large component for the visible cake model
  • 19 smaller components that were likely the hidden payload

After isolating the small components, the hidden geometry became much easier to inspect. Since the text was placed in 3D space at an angle, simple axis-aligned views were still suboptimal. PCA/SVD provided a better viewing plane.

The best readable projection was:

  • principal component 1 vs principal component 3

This corresponded to the helper image:

  • tasks/umasscybersec/Take a Slice/pc13.png

Finally, rendering the projected hidden triangles as filled polygons made the letters fully legible. That final step is visible in:

  • tasks/umasscybersec/Take a Slice/hidden_filled.png

The rendered text was:

UMASS{REDACTED}

Solution

1. Parse the binary STL

Binary STL is simple to parse. Read the header, triangle count, then unpack each 50-byte triangle record.

2. Build connected components

Collect each triangle's vertices, normalize them into hashable tuples, and build triangle adjacency by shared vertices. This separates the large visible model from disconnected hidden meshes.

3. Keep only the small hidden meshes

The payload was split across 19 small components. Removing the main cake component eliminated most of the visual clutter.

4. Project with PCA

The hidden geometry was not easiest to read from the standard XY/XZ/YZ views. PCA found a more natural plane aligned with the embedded text. The useful view was PC1 vs PC3.

5. Render filled triangles

A point cloud or wireframe view still leaves ambiguity. Filling the projected triangles produces solid glyphs, which makes the flag immediately readable.

Compact solve script:

#!/usr/bin/env python3
import struct
from collections import defaultdict, deque

import matplotlib.pyplot as plt
import numpy as np

PATH = "cake"


def read_stl(path):
    tris = []
    with open(path, "rb") as f:
        header = f.read(80)
        tri_count = struct.unpack("<I", f.read(4))[0]
        for _ in range(tri_count):
            rec = f.read(50)
            vals = struct.unpack("<12fH", rec)
            v1 = vals[3:6]
            v2 = vals[6:9]
            v3 = vals[9:12]
            tris.append(np.array([v1, v2, v3], dtype=np.float32))
    return np.array(tris)


def triangle_components(tris, decimals=5):
    vertex_to_tris = defaultdict(list)
    tri_vertices = []

    for i, tri in enumerate(tris):
        keys = []
        for v in tri:
            key = tuple(np.round(v, decimals))
            keys.append(key)
            vertex_to_tris[key].append(i)
        tri_vertices.append(keys)

    adj = [[] for _ in range(len(tris))]
    for keys in tri_vertices:
        touched = set()
        for key in keys:
            touched.update(vertex_to_tris[key])
        touched = list(touched)
        for a in touched:
            for b in touched:
                if a != b:
                    adj[a].append(b)

    seen = set()
    comps = []
    for start in range(len(tris)):
        if start in seen:
            continue
        q = deque([start])
        seen.add(start)
        comp = []
        while q:
            cur = q.popleft()
            comp.append(cur)
            for nxt in adj[cur]:
                if nxt not in seen:
                    seen.add(nxt)
                    q.append(nxt)
        comps.append(comp)
    return comps


def pca_basis(points):
    centered = points - points.mean(axis=0)
    _, _, vt = np.linalg.svd(centered, full_matrices=False)
    return centered, vt


def main():
    tris = read_stl(PATH)
    comps = triangle_components(tris)
    comps.sort(key=len, reverse=True)

    # largest component is the cake; keep the hidden ones
    hidden_idx = [i for comp in comps[1:] for i in comp]
    hidden = tris[hidden_idx]

    pts = hidden.reshape(-1, 3)
    centered, basis = pca_basis(pts)
    projected = centered @ basis.T

    # best view for this challenge: PC1 vs PC3
    x = projected[:, 0]
    y = projected[:, 2]

    fig, ax = plt.subplots(figsize=(12, 4))
    for tri in hidden:
        tri_centered = tri - pts.mean(axis=0)
        tri_proj = tri_centered @ basis.T
        poly = np.c_[tri_proj[:, 0], tri_proj[:, 2]]
        ax.fill(poly[:, 0], poly[:, 1], color="black", linewidth=0)

    ax.set_aspect("equal")
    ax.axis("off")
    plt.tight_layout()
    plt.savefig("hidden_filled.png", bbox_inches="tight", pad_inches=0)
    plt.show()


if __name__ == "__main__":
    main()

Running this against the isolated hidden geometry produced the readable filled rendering and revealed the flag.

</details>

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

signed by XESXOR