← Back to Writeups
HTBN/ASteganography

Hiding in Plain Sight

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

Hiding in Plain Sight

Platform: Metactf | Category: Steganography | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-10 | Status: Solved Techniques: block_average_downsample, face_identification, gaussian_blur, histogram_equalization, hybrid_image_reveal

Summary

AI-generated hybrid image where high-frequency details render a moss-covered Greek statue but low-frequency content hides a portrait of Barack Obama. Solved by Gaussian blur / heavy downsample + histogram equalization to reveal the hidden face.

Recon

Port scan

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

Enumeration highlights

  • Event: metactf | ID: 20260410_metactf_hiding_in_plain_sight
  • Tags: steganography, image, hybrid_image, optical_illusion, webp, blur, downsample, histogram_equalization, image_identification, ai_generated
  • Indicators: Task name: Hiding in Plain Sight, Hint: 'can't put my finger on it' (stop looking at details, look at the whole), 1024x1024 WebP image, single VP8 chunk, no EXIF / XMP / hidden chunks, Visible content: highly detailed moss-covered classical sculpture / Poseidon-style figure, No LSB / file-level steganography present
  • Source: 20260410_metactf_hiding_in_plain_sight.md

Foothold

Vulnerability / Misconfiguration

  1. Block_average_downsample
  2. Face_identification
  3. Gaussian_blur
  4. Histogram_equalization
  5. Hybrid_image_reveal
<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

  • block_average_downsample
  • face_identification
  • gaussian_blur
  • histogram_equalization
  • hybrid_image_reveal
  • Tags: steganography, image, hybrid_image, optical_illusion, webp, blur, downsample, histogram_equalization, image_identification, ai_generated

Original Writeup

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

Description

There's something strange about this image but I can't put my finger on it, any ideas? The flag will be the name of the person or object you find, in the format DawgCTF{Chicken_Sandwich}.

File: https://metaproblems.com/9158c536955b3b93c3b1ec47841cc0ff/hello.webp

The file is a 1024x1024 lossy WebP showing a detailed classical sculpture: a muscular bearded male figure (Poseidon / Neptune style) covered in green moss, set against a fountain / water background. Note that even though the event is metactf, the flag format is DawgCTF{...} — the challenge was reused from DawgCTF (UMBC). Always follow the format in the task description, not the event name.

Analysis

1. File-level recon — nothing hidden

Standard stego checks all came back empty:

file hello.webp
# RIFF (little-endian) data, Web/P image, VP8 encoding, 1024x1024, lossy

exiftool hello.webp
# Only size/dimensions, no EXIF / XMP / ICC

strings hello.webp | grep -iE 'flag|ctf|dawg|key|secret'
# nothing

Manually parsing WebP chunks confirms a single VP8 chunk with no extra EXIF, XMP , ICCP, ANIM, or ANMF chunks, and no trailing data after the RIFF container:

with open('hello.webp', 'rb') as f:
    data = f.read()
assert data[:4] == b'RIFF' and data[8:12] == b'WEBP'
i = 12
while i < len(data):
    cid = data[i:i+4]
    csize = int.from_bytes(data[i+4:i+8], 'little')
    print(cid, csize, i)
    i += 8 + csize + (csize & 1)  # chunk + padding
# VP8  85836 12
# End at 85856 == filesize

So this is not file-level / LSB / metadata stego. The "strange" thing has to be visual.

2. Reading the hint literally

  • Title: Hiding in Plain Sight
  • Description: "There's something strange about this image but I can't put my finger on it"

"Can't put your finger on it" = literally can't see it when you focus on details. That is the textbook description of a hybrid image: an image whose high-frequency content (edges, fine texture) shows one picture, while its low-frequency content (broad tones, coarse shapes) shows a different one. Your brain prefers the high-frequency version when you look closely, so the other picture only shows up when you blur, squint, downscale, or step back.

These are now cheap to produce with AI pipelines like Monster Labs QR-Monster ControlNet and Illusion Diffusion — they take a target "hidden" picture (e.g. a portrait) as a control signal and render a cover image (statue, landscape, …) whose luminance follows the target.

3. Revealing the hidden picture

Convert WebP → PNG for easier processing:

dwebp hello.webp -o hello.png
# 1024x1024, 8-bit RGB

Then attack the image with any low-pass filter. Three approaches that all work:

A. Gaussian blur

from PIL import Image, ImageFilter
img = Image.open('hello.png')
for r in [5, 10, 20, 30, 50]:
    img.filter(ImageFilter.GaussianBlur(radius=r)).save(f'hello_blur_{r}.png')

At radius ~25 a portrait clearly emerges in the middle/right of the frame.

B. Heavy downsample + upscale

tiny = img.resize((32, 32), Image.LANCZOS)     # or 48/64
big  = tiny.resize((512, 512), Image.LANCZOS)
big.save('hello_pixelated_32.png')

The 32x32 / 48x48 / 64x64 versions show a recognizable face.

C. Block-average downsample + histogram equalization (clearest)

import numpy as np
from PIL import Image, ImageOps
arr = np.array(Image.open('hello.png').convert('L'), dtype=np.float32)
factor = 16
h, w = arr.shape
nh, nw = h // factor, w // factor
small = arr[:nh*factor, :nw*factor].reshape(nh, factor, nw, factor).mean(axis=(1, 3))
out = Image.fromarray(small.astype(np.uint8)).resize((1024, 1024), Image.LANCZOS)
ImageOps.equalize(out).save('hello_avg_16_eq.png')

The block-average step throws away all the statue texture, and ImageOps.equalize stretches the contrast of what is left. The result is an unmistakable portrait of a man in a dark suit and tie, with a narrow face, prominent ears and short dark hair.

4. Identification

The distinctive features — short dark hair, narrow face, prominent ears, specific jaw/eyebrow shape, formal suit and tie — match Barack Obama, 44th President of the United States. The underlying "target" image looks like one of his well-known official / campaign portraits.

5. Flag formatting

The task gives DawgCTF{Chicken_Sandwich} as a format example: snake_case with an underscore between two words (First_Last).

Final flag: DawgCTF{REDACTED}

Solution (TL;DR)

  1. curl -O https://metaproblems.com/9158c536955b3b93c3b1ec47841cc0ff/hello.webp
  2. dwebp hello.webp -o hello.png
  3. Blur heavily (Gaussian r=20-40) or downscale to ~32-64 px
  4. Histogram-equalize for contrast
  5. Recognize Barack Obama in the low-frequency content
  6. Submit DawgCTF{REDACTED}

Key takeaways

  • When a stego image task says "look carefully", "step back", "can't put my finger on it", "squint", "from a distance", always try hybrid-image reveal first (Gaussian blur r~25, downscale to ~32-64 px, histogram equalization) before digging into bit planes, LSB, or file-level tricks.
  • numpy.reshape(...).mean(...) block-average downsampling gives a cleaner low-frequency view than a single LANCZOS resize, because it discards the high-frequency cover completely.
  • ImageOps.equalize on the blurred/downsampled result dramatically improves visibility of the hidden portrait.
  • File-level recon is still worth doing first (EXIF, chunks, strings, LSB) — it cheaply rules out the usual suspects before you commit to visual analysis.
  • Flag format is defined by the task description, not the event name. Here the event is metactf but the flag starts with DawgCTF{.
</details>

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

signed by XESXOR