← Back to Writeups
HTBN/ASteganography

Ninja-Nerds

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

Ninja-Nerds

Platform: Umasscybersec | Category: Steganography | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-11 | Status: Solved Techniques: bit_stream_reconstruction, single_channel_lsb_extraction

Summary

Task: a normal-looking PNG image contained no suspicious metadata, extra chunks, or useful strings, suggesting pixel-level hiding instead of container abuse. Solution: extract the least significant bits from the blue channel, group them MSB-first into bytes, and decode the recovered stream to read the flag directly.

Recon

Port scan

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

Enumeration highlights

  • Event: umasscybersec | ID: 20260411_umasscybersec_ninja_nerds
  • Tags: lsb, png, bit_plane, image_steganography, blue_channel
  • Indicators: PNG file with normal metadata and valid chunk structure, No useful output from strings or metadata inspection, Image challenge where only one RGB channel may carry hidden low-bit data, Flag appears directly after extracting a single color channel LSB stream
  • Source: 20260411_umasscybersec_ninja_nerds.md

Foothold

Vulnerability / Misconfiguration

  1. Bit_stream_reconstruction
  2. Single_channel_lsb_extraction
<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

  • bit_stream_reconstruction
  • single_channel_lsb_extraction
  • Tags: lsb, png, bit_plane, image_steganography, blue_channel

Original Writeup

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

Description

Organizer description was not preserved in the local task files.

The challenge provided a single PNG image, challenge.png. Basic file and metadata checks were clean, so the goal was to determine whether the payload was hidden inside image pixel data rather than in appended data or unusual PNG chunks.

Analysis

Initial recon showed a normal PNG:

file challenge.png
# PNG image data, 640 x 360, 8-bit/color RGB, non-interlaced

exiftool challenge.png
# normal metadata only

pngcheck challenge.png
# valid PNG, no suspicious chunks

strings challenge.png
# nothing useful

That ruled out the easiest container-level tricks. Since the file structure looked clean, the remaining likely hiding places were the RGB pixel channels and their bit planes.

Reviewing similar stego writeups for PNG bit-plane analysis suggested checking channels independently instead of treating the image as one flat byte stream. That mattered here because the payload was not spread across all pixels equally: it was stored only in the least significant bit of the blue channel.

The second key detail was byte assembly order. Grouping the extracted bits MSB-first into bytes produced readable text immediately, including the full flag.

Solution

1. Verify the PNG is structurally normal

Use file, exiftool, pngcheck, and strings to confirm there is no obvious metadata leak, appended archive, or suspicious custom PNG chunk.

2. Extract one channel at a time

Treat the image as an RGB array and isolate the blue channel (arr[:, :, 2]). Then keep only its least significant bit with & 1.

3. Rebuild bytes from the bit stream

Flatten the bit plane, take 8 bits at a time, and combine them MSB-first into bytes.

4. Decode and search for the flag

Decode the recovered byte stream with a permissive single-byte codec such as latin-1, then regex-search for the UMASS{...} pattern.

Full working solve script:

#!/usr/bin/env python3
import re

import numpy as np
from PIL import Image


def main():
    arr = np.array(Image.open("challenge.png"))
    bits = (arr[:, :, 2] & 1).flatten()

    data = bytearray()
    for i in range(0, len(bits) - 7, 8):
        byte = 0
        for bit in bits[i:i + 8]:
            byte = (byte << 1) | int(bit)
        data.append(byte)

    text = data.decode("latin-1", errors="ignore")
    match = re.search(r"UMASS\{[^}]+\}", text)
    if not match:
        raise SystemExit("flag not found")

    print(match.group(0))


if __name__ == "__main__":
    main()

Running it prints:

UMASS{REDACTED}
</details>

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

signed by XESXOR