In the previous challenge, we recovered a hidden document from a JPEG image using Steghide and StegSeek. Challenge 3 takes a different approach. Instead of relying on a single hidden file, we need to investigate a JPEG containing several embedded data structures, carve those structures manually, and follow multiple layers of encryption and steganography to recover the flag.
This walkthrough covers the investigation from the initial file checks to the final archive extraction. We will use tools such as file, ExifTool, Binwalk, xxd, dd, OpenSSL, and a Python extraction script supplied with the challenge.
Lab note: The filenames, offsets, and password pattern in this article belong to this challenge. Your results may differ when analysing another file. Run these steps only against files and systems you own or are authorised to investigate.
Understanding the Challenge
Prepare the Challenge Files
Move into the directory for Challenge 3 in the downloaded challenge repository. The exact path depends on where you saved the repository. In this example, we are working from the challenge directory and using Honey.jpeg as the target image.
List the available files:
┌──(kali㉿kali)-[~/steganography/steganography-ctf/participant/challenges]
└─$ cd 03-steganography-lvl-3
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ ls -al
total 284
drwxrwxr-x 2 kali kali 4096 Oct 28 12:47 .
drwxrwxr-x 6 kali kali 4096 Oct 28 12:47 ..
-rw-rw-r-- 1 kali kali 2452 Oct 28 12:47 BRIEF.md
-rw-rw-r-- 1 kali kali 277078 Oct 28 12:47 Honey.jpeg
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The directory contains two important files: the challenge description and the target JPEG image.
Read the description before proceeding. It may contain clues that become useful later in the investigation.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ cat BRIEF.md
Well now… this here's a simple one, leastways it looks that way. Just one lonely JPEG, sittin' there waitin' to be picked over. Your job's plain enough — find the flag. But I'll tell you, partner, there ain't much in this world that's as simple as it looks. Plenty of folks've gone diggin' through fossils like this and never come back with so much as a whisper.
Now, if you get yourself stuck, and you ain't got nothin' better to do, lean back a spell, and I'll spin you a yarn about a feller they called John, back in the Desert Storm days.
They say John could stare down a locked file the way a cowboy sizes up a spooked horse — patient, calm, and just a little amused. He wasn't ridin' solo, neither. Had a few sharp pals who shared his taste for codes and riddles — Aho, Weinberger, and Kernighan. Together, they were somethin' of a posse — the kind who could turn scrambled lines into sense faster than a dust devil changes course.
When John needed a way to tie loose ends together, those three showed him a clever trick: how to stitch two strings — a word and a tail — into one clean rope. He liked that trick so much he'd jot their initials — A W K — in the margins of his notebook, a quiet grin on his face, like he was leavin' himself a joke only he'd get.
John, he always found ways to make work into play. He'd tell the young codebreakers, "Don't just pick a name — build one. Give it a backbone, then tack on somethin' small that makes it yours." To prove his point, he'd show 'em how to make a password that wasn't just strong, but had a rhythm to it: start with a codename, add a #, then toss on two numbers and a three-letter tag that danced between lower and upper case. Made sense to him — a little structure, a little flair, and a lesson tucked inside the challenge.
There was even a friend of his — a real peculiar fella — who wrote in fancy, alien-lookin' symbols, said you could describe any pattern with nothin' but question marks and letters. Folks these days call it mask shorthand or somethin' close to it. And if you peeked at John's old notebook, you'd see one of his patterns scribbled out like this:
?d?d?l?u?l
Well, I hope ya enjoyed an old man's tale about puzzles, little suffixes, and how sometimes the smallest patterns can open the biggest doors. So go on now. The JPEG's waitin'.
File: Honey.jpeg
Flag format: Flag{...}
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
For the initial examination, we will focus on Honey.jpeg. We can return to the description when we need to interpret the challenge-specific clues.
Inspect the Image Format
A file extension does not prove a file's actual format. A text document or encrypted file can be renamed with a `.jpg` extension without becoming a JPEG image. We should inspect the file contents before trusting the extension.
Use the `file` command:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ file Honey.jpeg
Honey.jpeg: JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=6, orientation=upper-left, xresolution=86, yresolution=94, resolutionunit=2], baseline, precision 8, 1024x1024, components 3
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The challenge file is identified as JPEG image data using the EXIF standard, with dimensions of 1024 × 1024 pixels. This confirms that the file contains a JPEG image structure.
However, a valid JPEG can still contain additional data. The next step is to inspect its metadata.
Inspect the JPEG Metadata
Image metadata can include information such as the camera model, image dimensions, orientation, date and time, colour space, and—in some cases—location details. Metadata can also contain clues deliberately added to a CTF challenge.
Use ExifTool to inspect the image:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ exiftool Honey.jpeg
ExifTool Version Number : 13.55
File Name : Honey.jpeg
Directory : .
File Size : 277 kB
File Modification Date/Time : 2026:10:28 12:47:16-04:00
File Access Date/Time : 2026:10:28 22:42:35-04:00
File Inode Change Date/Time : 2026:10:28 12:47:16-04:00
File Permissions : -rw-rw-r--
File Type : JPEG
File Type Extension : jpg
MIME Type : image/jpeg
Exif Byte Order : Big-endian (Motorola, MM)
Orientation : Horizontal (normal)
X Resolution : 72
Y Resolution : 72
Resolution Unit : inches
Y Cb Cr Positioning : Centered
Exif Version : 0221
Components Configuration : Y, Cb, Cr, -
Flashpix Version : 0100
Color Space : sRGB
Exif Image Width : 1024
Exif Image Height : 1024
Scene Capture Type : Standard
Image Width : 1024
Image Height : 1024
Encoding Process : Baseline DCT, Huffman coding
Bits Per Sample : 8
Color Components : 3
Y Cb Cr Sub Sampling : YCbCr4:2:0 (2 2)
Image Size : 1024x1024
Megapixels : 1.0
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The output displays the image's technical properties, including its dimensions, EXIF version, orientation, colour space, and JPEG encoding information.
In this challenge, the metadata does not reveal an obvious hidden message. In particular, there is no useful encrypted message in the EXIF Comment field.
That does not mean the image is clean. Metadata is only one place where information can be stored. Additional content may be appended to the file or embedded between its normal structures. To investigate the complete binary file, we need a signature-scanning tool.
Scan the Image with Binwalk
Binwalk searches files for recognisable signatures that may indicate embedded files or data structures. For example, a JPEG may contain a ZIP archive, another image, or an encrypted data section.
Run a standard Binwalk scan:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ binwalk Honey.jpeg
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 JPEG image data, EXIF standard
12 0xC TIFF image data, big-endian, offset of first image directory: 8
235921 0x39991 OpenSSL encryption, salted, salt: 0x5DADF5CAE236274
246742 0x3C3D6 Zip archive data, at least v2.0 to extract, compressed size: 57, uncompressed size: 58, name: do_not_open.txt
246957 0x3C4AD End of Zip archive, footer length: 22
246979 0x3C4C3 JPEG image data, JFIF standard 1.01
272710 0x42946 OpenSSL encryption, salted, salt: 0xCEBB0C23B9EB9F1
272950 0x42A36 OpenSSL encryption, salted, salt: 0x935A1741FA39105C
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The output contains three columns:
- Decimal: The byte offset where Binwalk identifies a structure, shown in decimal notation.
- Hexadecimal: The same offset represented in hexadecimal notation.
- Description: The type of structure or file signature detected.
The important findings in this challenge are:
The entries at offsets 235,921, 272,710, and 272,950 are especially interesting. They indicate three sections that appear to use OpenSSL's salted encryption format. Binwalk also identifies a ZIP archive and a second JPEG image.
These offsets mark the detected starting positions. They do not, by themselves, tell us the exact length of every structure. We must examine the boundaries before extracting each section.
Try Binwalk's Automatic Extraction
Binwalk can attempt to extract recognised embedded files. The -e option enables extraction, while -M enables recursive scanning, sometimes called Matryoshka extraction. With recursive extraction, Binwalk scans extracted files again for additional embedded structures.
Run:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ mkdir -p carved
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ binwalk -eM Honey.jpeg
Scan Time: 2026-10-28 22:49:56
Target File: /home/kali/steganography/steganography-ctf/participant/challenges/03-steganography-lvl-3/Honey.jpeg
MD5 Checksum: 609fd311705fe0c46a0985cd2bb807a0
Signatures: 436
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
246742 0x3C3D6 Zip archive data, at least v2.0 to extract, compressed size: 57, uncompressed size: 58, name: do_not_open.txt
WARNING: One or more files failed to extract: either no utility was found or it's unimplemented
Scan Time: 2026-10-28 22:49:56
Target File: /home/kali/steganography/steganography-ctf/participant/challenges/03-steganography-lvl-3/_Honey.jpeg.extracted/do_not_open.txt
MD5 Checksum: bb91b2890e51b7c3d32e2d495389379a
Signatures: 436
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Binwalk creates an extraction directory, usually named: _Honey.jpeg.extracted/
The output also reports that one or more items could not be extracted. This does not mean the signature scan was wrong. Identifying a structure and extracting or decrypting it are separate operations.
Binwalk may recognise an OpenSSL header but cannot determine the password needed to decrypt the data. It may also lack the extraction utility or rule required for a particular structure.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ ls -al
total 288
drwxrwxr-x 3 kali kali 4096 Oct 28 22:52 .
drwxrwxr-x 6 kali kali 4096 Oct 28 12:47 ..
-rw-rw-r-- 1 kali kali 2452 Oct 28 12:47 BRIEF.md
-rw-rw-r-- 1 kali kali 277078 Oct 28 12:47 Honey.jpeg
drwxrwxr-x 2 kali kali 4096 Oct 28 22:52 _Honey.jpeg.extracted
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Inspect the files that were extracted. Change the directory and list all files and directories.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ cd _Honey.jpeg.extracted
┌──(kali㉿kali)-[~/…/participant/challenges/03-steganography-lvl-3/_Honey.jpeg.extracted]
└─$ ls -al
total 44
drwxrwxr-x 2 kali kali 4096 Oct 28 22:52 .
drwxrwxr-x 3 kali kali 4096 Oct 28 22:52 ..
-rw-rw-r-- 1 kali kali 30336 Oct 28 22:52 3C3D6.zip
-rw-r--r-- 1 kali kali 58 Sep 28 2025 do_not_open.txt
┌──(kali㉿kali)-[~/…/participant/challenges/03-steganography-lvl-3/_Honey.jpeg.extracted]
└─$
In this challenge, the directory contains:
Inspect do_not_open.txt
Now, we have to inspect the text file to get any clues:
┌──(kali㉿kali)-[~/…/participant/challenges/03-steganography-lvl-3/_Honey.jpeg.extracted]
└─$ cat do_not_open.txt
EMVVBFGIATMDATUGEO
TL: Honey
TR: Badger
BL: Dont
BR: Care
┌──(kali㉿kali)-[~/…/participant/challenges/03-steganography-lvl-3/_Honey.jpeg.extracted]
└─$
Its contents may look like a clue, but we should not assume that it is relevant. The directory also contains a ZIP archive, so inspect the archive before spending time on the text file.
Inspect the extracted ZIP archive
List the archive contents without extracting them:
┌──(kali㉿kali)-[~/…/participant/challenges/03-steganography-lvl-3/_Honey.jpeg.extracted]
└─$ unzip 3C3D6.zip
Archive: 3C3D6.zip
replace do_not_open.txt? [y]es, [n]o, [A]ll, [N]one, [r]ename: n
┌──(kali㉿kali)-[~/…/participant/challenges/03-steganography-lvl-3/_Honey.jpeg.extracted]
└─$
The archive contains another file named do_not_open.txt. It is the same decoy file already found in the extraction directory. This branch does not lead to the main solution, so we can set it aside and return to the structures reported by the original Binwalk scan.
Return to the challenge directory:
┌──(kali㉿kali)-[~/…/participant/challenges/03-steganography-lvl-3/_Honey.jpeg.extracted]
└─$ cd ..
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ ls -al
total 288
drwxrwxr-x 3 kali kali 4096 Oct 28 22:52 .
drwxrwxr-x 6 kali kali 4096 Oct 28 12:47 ..
-rw-rw-r-- 1 kali kali 2452 Oct 28 12:47 BRIEF.md
-rw-rw-r-- 1 kali kali 277078 Oct 28 12:47 Honey.jpeg
drwxrwxr-x 2 kali kali 4096 Oct 28 22:52 _Honey.jpeg.extracted
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Prepare for Manual File Carving
Binwalk identified multiple structures, but automatic extraction did not recover everything. We will therefore extract the required byte ranges ourselves. This process is known as file carving.
File carving is a digital forensics technique used to recover data from a larger file by identifying a structure's starting position and ending boundary. We can then copy only that range into a separate output file.
For this task, we will use two Linux utilities:
- `xxd` displays file contents in hexadecimal form. It is useful for checking file signatures and inspecting bytes at specific offsets.
- `dd` copies selected data from an input file to an output file. By setting the starting offset and byte count, we can carve an individual structure from the original image.
Create a directory for the recovered sections:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ mkdir -p carved
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Check the original file size
We need the original file size to calculate the length of the final section, which continues to the end of `Honey.jpeg`. Use `stat`:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ stat -c %s Honey.jpeg
277078
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The file size is: 277078
Keep this value. We will use it when carving the final encrypted section.
Understand the `xxd` options
The general form used in this walkthrough is:
xxd -s <offset> -l <number of bytes> <input file>
Here:
- `-s` sets the starting offset.
- `-l` sets the number of bytes to display.
For example, `xxd -s 235921 -l 16 Honey.jpeg` displays 16 bytes beginning at decimal offset 235,921.
Understand the `dd` options
The general form for our carving commands is:
dd if=<input-file> of=<output-file> bs=1 skip=<starting-offset> count=<number-of-bytes> status=progress
The options mean:
- `if` specifies the input file.
- `of` specifies the output file.
- `bs=1` sets the block size to one byte, allowing precise byte-based offsets.
- `skip` tells `dd` how many bytes to skip from the beginning of the input file.
- `count` specifies how many bytes to copy after the skipped bytes.
- `status=progress` displays the copy progress.
To calculate the number of bytes to carve, subtract the starting offset from the ending boundary:
Length = Ending boundary - Starting offset
For example, a section beginning at offset 100 and ending at boundary 200 has a length of 100 bytes. The ending boundary is exclusive: the byte at that boundary belongs to the next section and is not included in the current extraction.
Do not assume that the next Binwalk result always marks the exact end of the current structure.
There may be padding, unrelated bytes, comments, or other data between structures. Inspect the actual signatures and boundaries before choosing a byte count.
Carve the First OpenSSL-Encrypted Section
Binwalk reports the first encrypted section at decimal offset 235,921. Before extracting it, inspect its first 16 bytes:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ xxd -s 235921 -l 16 Honey.jpeg
00039991: 5361 6c74 6564 5f5f 5dad f5ca 0e23 6274 Salted__]....#bt
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The output begins with: _salted.
This marker is commonly found in OpenSSL's password-based encryption file format. It is followed by salt information used by the key-derivation process.
Calculate the section length
The next ZIP archive begins at offset 246,742. However, the boundary established during this investigation for the encrypted section is 246,721, leaving a 21-byte gap before the ZIP archive begins.
Use the verified ending boundary rather than assuming the encrypted data continues directly to the ZIP signature:
- Ending boundary: 246721
- Starting offset: 235921
- Length: 246721 - 235921 = 10800 bytes
The length is 10,800 bytes. It is divisible by 16, which is consistent with the block alignment expected for an AES-CBC encrypted payload, including the 16-byte OpenSSL salted header. This is a useful consistency check, although block alignment alone does not prove the encryption algorithm.
Extract the encrypted section
Run:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ dd if=Honey.jpeg of=carved/secret.enc bs=1 skip=235921 count=10800 status=progress
10800+0 records in
10800+0 records out
10800 bytes (11 kB, 11 KiB) copied, 0.0382461 s, 282 kB/s
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Verify the recovered file:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ file carved/secret.enc
carved/secret.enc: openssl enc'd data with salted password
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The extracted file should retain the OpenSSL `Salted__` header.
Carve the ZIP Archive
The ZIP archive begins at decimal offset 246,742. To determine its ending boundary, Binwalk's output identifies a ZIP end-of-central-directory record at offset 246,957.
First, inspect the beginning of the archive:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ xxd -s 246742 -l 16 Honey.jpeg
0003c3d6: 504b 0304 1400 0000 0800 b99c 3c5b 5fef PK..........<[_.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The bytes identify the start of a ZIP local file header. Next, inspect the end-of-central-directory record:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ xxd -s 246957 -l 16 Honey.jpeg
0003c4ad: 504b 0506 0000 0000 0100 5500 0000 PK..........U ...
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The record begins with the ZIP end-of-central-directory signature. In this challenge, the record is 22 bytes long and has no additional ZIP comment. Therefore:
246957 + 22 = 246979
The calculated ending boundary, 246,979, matches the starting offset of the next JPEG image reported by Binwalk.
This confirms the boundary for this archive. In other ZIP files, the end-of-central-directory record may include a comment, so its comment-length field must also be checked rather than assuming the record is always exactly 22 bytes.
Calculate the ZIP archive's length:
- Ending boundary: 246979
- Starting offset: 246742
- Length: 246979 - 246742 = 237 bytes
Extract the archive:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ dd if=Honey.jpeg of=carved/decoy.zip bs=1 skip=246742 count=237 status=progress
237+0 records in
237+0 records out
237 bytes copied, 0.000447852 s, 529 kB/s
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Verify and test it:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ file carved/decoy.zip
carved/decoy.zip: Zip archive data, made by v3.0 UNIX, extract using at least v2.0, last modified Sep 28 2025 19:37:50, uncompressed size 58, method=deflate
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The extracted file should be identified as a ZIP archive, and the integrity test should complete successfully. Extract it into a separate directory if you want to inspect its contents:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ unzip carved/decoy.zip
Archive: carved/decoy.zip
inflating: do_not_open.txt
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ ls -al
total 296
drwxrwxr-x 4 kali kali 4096 Nov 6 00:53 .
drwxrwxr-x 6 kali kali 4096 Oct 28 12:47 ..
-rw-rw-r-- 1 kali kali 2452 Oct 28 12:47 BRIEF.md
drwxrwxr-x 2 kali kali 4096 Nov 6 00:51 carved
-rw-r--r-- 1 kali kali 58 Sep 28 2025 do_not_open.txt
-rw-rw-r-- 1 kali kali 277078 Oct 28 12:47 Honey.jpeg
drwxrwxr-x 2 kali kali 4096 Nov 4 10:25 _Honey.jpeg.extracted
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ cat do_not_open.txt
EMVVBFGIATMDATUGEO
TL: Honey
TR: Badger
BL: Dont
BR: Care
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The archive contains `do_not_open.txt`, the same decoy file discovered during Binwalk's automatic extraction. We have now confirmed that this ZIP archive is not the main route to the flag.
Carve the Second JPEG Image
The next embedded structure begins at offset 246,979 and is another JPEG image.
Check its starting bytes:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ xxd -s 246979 -l 16 Honey.jpeg
0003c4c3: ffd8 ffe0 0010 4a46 4946 0001 0100 0001 ......JFIF......
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
A JPEG image normally begins with the start-of-image marker FF D8. The output confirms that marker at the expected location.
Find the JPEG ending boundary
The next encrypted structure begins at offset 272,710. To check the bytes immediately before that offset, inspect the preceding 16 bytes:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ dd if=Honey.jpeg of=carved/nothingtoseehere.jpg bs=1 skip=246979 count=25731 status=progress
25731+0 records in
25731+0 records out
25731 bytes (26 kB, 25 KiB) copied, 0.08553 s, 301 kB/s
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The output includes the JPEG end-of-image marker, FF D9, at the end of the range. This indicates that the JPEG ends immediately before offset 272,710.
Calculate the image length:
- Ending boundary: 272710
- Starting offset: 246979
- Length: 272710 - 246979 = 25731 bytes
Extract and inspect the image:
Carve the image into a separate file:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ dd if=Honey.jpeg of=carved/nothingtoseehere.jpg bs=1 skip=246979 count=25731 status=progress
25731+0 records in
25731+0 records out
25731 bytes (26 kB, 25 KiB) copied, 0.08553 s, 301 kB/s
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Verify the recovered file and inspect its metadata:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ file carved/nothingtoseehere.jpg
carved/nothingtoseehere.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, progressive, precision 8, 311x572, components 3
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Also, we use exiftool to understand:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ exiftool carved/nothingtoseehere.jpg
ExifTool Version Number : 13.55
File Name : nothingtoseehere.jpg
Directory : carved
File Size : 26 kB
File Modification Date/Time : 2026:11:06 00:57:11-05:00
File Access Date/Time : 2026:11:06 00:57:23-05:00
File Inode Change Date/Time : 2026:11:06 00:57:11-05:00
File Permissions : -rw-rw-r--
File Type : JPEG
File Type Extension : jpg
MIME Type : image/jpeg
JFIF Version : 1.01
Resolution Unit : None
X Resolution : 1
Y Resolution : 1
Image Width : 311
Image Height : 572
Encoding Process : Progressive DCT, Huffman coding
Bits Per Sample : 8
Color Components : 3
Y Cb Cr Sub Sampling : YCbCr4:2:0 (2 2)
Image Size : 311x572
Megapixels : 0.178
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The extracted file is a JPEG image. Its metadata does not reveal an obvious clue, so we will retain it for later analysis. It becomes important when we investigate the JPEG quantization tables.
Carve the Fourth Structure: Another Encrypted Section
Binwalk identifies another OpenSSL-encrypted structure at offset 272,710.
Inspect its first 16 bytes:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ xxd -s 272710 -l 16 Honey.jpeg
00042946: 5361 6c74 6564 5f5f 0ceb b0c2 3b9e b9f1 Salted__....;...
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Once again, the data begins with the `Salted__` marker.
The next encrypted section begins at offset 272,950. For this extraction, the established boundary of the current section is the start of that next structure.
Calculate the length:
- Ending boundary: 272950
- Starting offset: 272710
- Length: 272950 - 272710 = 240 bytes
The 240-byte length is divisible by 16. This is consistent with the block alignment expected for an OpenSSL salted AES-CBC payload, but it is not, by itself, conclusive proof of the cipher.
Extract the section:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ dd if=Honey.jpeg of=carved/payload.enc bs=1 skip=272710 count=240 status=progress
240+0 records in
240+0 records out
240 bytes copied, 0.000854182 s, 281 kB/s
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Verify the result:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ file carved/payload.enc
carved/payload.enc: openssl enc'd data with salted password
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The extracted data should begin with the same OpenSSL salted header.
Carve the Fifth and Final Structure
The final encrypted section begins at offset 272,950.
First, confirm its starting bytes:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ xxd -s 272950 -l 16 Honey.jpeg
00042a36: 5361 6c74 6564 5f5f 935a 1741 fa39 105c Salted__.Z.A.9.\
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The `Salted__` header appears again.
Unlike the previous sections, there is no later embedded structure that marks the ending boundary. This section continues to the end of the original file. We therefore use the file size obtained earlier: 277,078 bytes.
Calculate the number of bytes remaining:
- File size: 277078
- Starting offset: 272950
- Length: 277078 - 272950 = 4128 bytes
Extract the final section:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ dd if=Honey.jpeg of=carved/decoy_random.enc bs=1 skip=272950 status=progress
4128+0 records in
4128+0 records out
4128 bytes (4.1 kB, 4.0 KiB) copied, 0.0161424 s, 256 kB/s
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Verify the file and its header:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ file carved/decoy_random.enc
carved/decoy_random.enc: openssl enc'd data with salted password
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The output should be 4,128 bytes long and should begin with the OpenSSL salted marker. The earlier JPEG metadata inspection did not reveal a useful clue, so we can leave that image aside for now.
We have now recovered all five structures identified during the initial scan.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ ls -al carved/*
-rw-rw-r-- 1 kali kali 4128 Nov 6 12:59 carved/decoy_random.enc
-rw-rw-r-- 1 kali kali 237 Nov 6 12:45 carved/decoy.zip
-rw-rw-r-- 1 kali kali 25731 Nov 6 12:53 carved/nothingtoseehere.jpg
-rw-rw-r-- 1 kali kali 240 Nov 6 12:56 carved/payload.enc
-rw-rw-r-- 1 kali kali 10800 Nov 6 12:41 carved/secret.enc
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The table lists the byte ranges used in this walkthrough. The ending boundary is exclusive, meaning the byte at the ending offset is not included in the extracted file.
At this stage, we have three encrypted files, one ZIP archive, and one additional JPEG image. The ZIP archive is a decoy. The next step is to decrypt the first OpenSSL file and examine the material inside it.
Decrypt the secret.enc
From our earlier investigation, we already know that this
file contains OpenSSL-encrypted data with a salted header. This means we cannot simply open it and read its original
contents.
We need the correct decryption settings and, most
importantly, the password.
So, where do we find the password?
This is where the challenge description becomes useful. Remember, the challenge brief contains some clues about the
password format.
Let's examine those clues carefully.
The challenge description contains a password pattern. It specifies a codename followed by a hash symbol and five characters with particular character types:
For this challenge:
- `?d` represents a digit.
- `?l` represents a lowercase letter.
- `?u` represents an uppercase letter.
The codename is Desert Storm. The pattern requires two digits, then a lowercase letter, an uppercase letter, and another lowercase letter. Applying the challenge's intended formatting gives this password:
The password omits the space in the displayed codename.
Now, we've got a possible password that matches the pattern
provided in the challenge. Let's use it to decrypt the first encrypted file.
Decrypt the OpenSSL File
The first carved encrypted file is `carved/secret.enc`. Its `Salted__` header indicates that we should use OpenSSL's password-based decryption workflow.
Run:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ openssl enc -d -aes-256-cbc -pbkdf2 -pass pass:'DesertStorm#82pLm' -in carved/secret.enc -out secret_bundle.bin
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The options are:
- `enc` selects the symmetric encryption command.
- `-d` enables decryption.
- `-aes-256-cbc` selects AES-256 in CBC mode.
- `-pbkdf2` enables PBKDF2 password-based key derivation.
- `-pass` supplies the password.
- `-in` specifies the encrypted input file.
- `-out` specifies the decrypted output file.
Identify the recovered file:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ ls -al
total 308
drwxrwxr-x 5 kali kali 4096 Nov 6 13:04 .
drwxrwxr-x 6 kali kali 4096 Oct 28 12:47 ..
-rw-rw-r-- 1 kali kali 2452 Oct 28 12:47 BRIEF.md
drwxrwxr-x 2 kali kali 4096 Nov 6 12:59 carved
drwxrwxr-x 2 kali kali 4096 Nov 6 12:46 decoy
-rw-rw-r-- 1 kali kali 277078 Oct 28 12:47 Honey.jpeg
drwxrwxr-x 2 kali kali 4096 Nov 4 10:25 _Honey.jpeg.extracted
-rw-rw-r-- 1 kali kali 10783 Nov 6 13:04 secret_bundle.bin
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ file secret_bundle.bin
secret_bundle.bin: Zip archive data, made by v3.0 UNIX, extract using at least v2.0, last modified Sep 28 2025 19:11:54, uncompressed size 8726, method=deflate
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The output identifies it as a ZIP archive. We have successfully decrypted the first encrypted section, but we still need to inspect the archive contents.
Test and extract the archive
Test the archive without extracting it:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ unzip -t secret_bundle.bin
Archive: secret_bundle.bin
testing: qtbl.py OK
testing: STEGO_KEY_368.txt OK
testing: passwords.enc OK
testing: iv.bin OK
No errors detected in compressed data of secret_bundle.bin.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The archive passes the integrity test. Extract it into a separate directory:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ unzip secret_bundle.bin -d secret_bundle
Archive: secret_bundle.bin
inflating: secret_bundle/qtbl.py
inflating: secret_bundle/STEGO_KEY_368.txt
extracting: secret_bundle/passwords.enc
extracting: secret_bundle/iv.bin
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
List the recovered files:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ ls -al secret_bundle/*
-rw-r--r-- 1 kali kali 16 Sep 29 2025 secret_bundle/iv.bin
-rw-r--r-- 1 kali kali 4960 Sep 29 2025 secret_bundle/passwords.enc
-rw-r--r-- 1 kali kali 8726 Sep 28 2025 secret_bundle/qtbl.py
-rw-r--r-- 1 kali kali 4824 Sep 28 2025 secret_bundle/STEGO_KEY_368.txt
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The directory contains four files:
- iv.bin
- passwords.enc
- qtbl.py
- STEGO_KEY_368.txt
The filenames provide useful leads:
- `iv.bin` appears to contain an initialization vector.
- `passwords.enc` contains encrypted password data.
- `qtbl.py` is a Python script that extracts hidden information from JPEG quantization tables.
- `STEGO_KEY_368.txt` contains data that appears to be used to construct the script's extraction key.
These filenames suggest that the files are related, but we
shouldn't assume their purpose just from their names. We will examine these files rather than assuming their purpose from their names alone.
Inspect the Encrypted Password File
Start by identifying the format of `passwords.enc`:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ file secret_bundle/passwords.enc
secret_bundle/passwords.enc: data
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The `file` utility does not recognise a familiar format. That does not necessarily indicate corruption. The file may contain raw encrypted bytes without a standard file header.
Inspect its first 32 bytes:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ xxd -l 32 secret_bundle/passwords.enc
00000000: a3f6 b20a e682 a56b 7ad9 64fb cd63 32d2 .......kz.d..c2.
00000010: 1f03 bffc 0751 1cca 02bf f37d eb40 ee8b .....Q.....}.@..
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The output looks like ciphertext, not readable text. Unlike `secret.enc`, it does not begin with the OpenSSL `Salted__` marker.
This suggests that `passwords.enc` may use raw encryption data with the key and initialization vector supplied separately.
What does this mean? It means that when we have the correct AES key and initialization vector, or IV, we can use OpenSSL to decrypt the encrypted file, provided we also specify the correct encryption algorithm and mode.
Finding and Verifying the AES Key and IV Before Decryption
AES encryption requires a key. In CBC mode, it also uses an initialization vector, commonly abbreviated as IV.
The key is the secret value used to encrypt and decrypt the data. The IV is an additional input used by CBC mode. For AES-CBC, the IV must be 16 bytes long, regardless of whether the AES key is 128, 192, or 256 bits.
Here, in this directory, we have an IV file. Inspect the IV file:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ xxd secret_bundle/iv.bin
00000000: 6e21 20b8 9a2a ba5f 75e0 b99d dc32 fba1 n! ..*._u....2..
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The displayed hexadecimal data contains 32 hexadecimal characters. Each pair of hexadecimal characters represents one byte, so the file contains 16 bytes.
Confirm the exact size:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ wc -c secret_bundle/iv.bin
16 secret_bundle/iv.bin
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The result is: 16 secret_bundle/iv.bin
This matches the required IV size for AES-CBC. However, the IV is not the AES key. We still need to recover the key before we can decrypt `passwords.enc`.
Understand the Python script
Before examining the key file, inspect the supplied Python script:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ mousepad secret_bundle/qtbl.py
You can also display it directly with `cat`, although `less` is usually easier for longer scripts. Press `q` to exit `less`.
The script reads a JPEG image and searches for its DQT, or Define Quantization Table, data. Quantization tables are part of JPEG compression. The script examines selected values in those tables and extracts their two least significant bits to reconstruct hidden bytes.
This is a form of steganography: information is concealed in values that are part of the JPEG structure, rather than being stored as a normal visible message.
The script also accepts an optional key. According to its implementation, the supplied value is used in an XOR operation on the extracted bytes to reverse an additional layer of obfuscation. The recovered bytes are then written to an output file.
In other words, `qtbl.py` is not simply reading ordinary image metadata. It is designed to recover data hidden inside the JPEG quantization tables.
Now, let's see how we're supposed to use it.
Read the script's usage information
Run the script's help command:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ python secret_bundle/qtbl.py
usage: qtbl.py [-h] {embed,extract} ...
positional arguments:
{embed,extract}
options:
-h, --help show this help message and exit
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
As you can see, it displays its help information.
Here, there is only 1 option, which is --help, not good for the task. So, let me take a look at the help option for extract.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ python secret_bundle/qtbl.py extract --help
usage: qtbl.py extract [-h] -i INPUT -o OUTPUT [-k KEY]
options:
-h, --help show this help message and exit
-i, --input INPUT
-o, --output OUTPUT
-k, --key KEY
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The extraction operation accepts an input image, an output filename, and a key. The relevant options are:
- `-i` specifies the input image.
- `-o` specifies the output file.
- `-k` supplies the extraction key.
Fortunately, we already have the image recovered from our
previous steps. It's named `carved/nothingtoseehere.jpg`.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ ls -al carved/*
-rw-rw-r-- 1 kali kali 4128 Nov 6 12:59 carved/decoy_random.enc
-rw-rw-r-- 1 kali kali 237 Nov 6 12:45 carved/decoy.zip
-rw-rw-r-- 1 kali kali 25731 Nov 6 12:53 carved/nothingtoseehere.jpg
-rw-rw-r-- 1 kali kali 240 Nov 6 12:56 carved/payload.enc
-rw-rw-r-- 1 kali kali 10800 Nov 6 12:41 carved/secret.enc
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
But what about the key? While looking to get hint or clue to get the key, I remembered a interesting file, here, we need
to investigate the file which is inside our extracted directory.
It's named STEGO_KEY_368.txt.
Construct the Extraction Key from `STEGO_KEY_368.txt`
The file `STEGO_KEY_368.txt` is the next clue. Its name contains the numbers 3, 6, and 8, which may indicate which records we need to select.
First, inspect the file:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ less secret_bundle/STEGO_KEY_368.txt
The contents appear as one long, difficult-to-read string. Press `q` to exit.
Check the number of lines and bytes
Count the newline characters with:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ wc -l secret_bundle/STEGO_KEY_368.txt
0 secret_bundle/STEGO_KEY_368.txt
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The result is zero. This does not mean the file is empty. It means the file contains no newline characters separating its contents into multiple lines.
Now check the total size in bytes:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ wc -c secret_bundle/STEGO_KEY_368.txt
4824 secret_bundle/STEGO_KEY_368.txt
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The file contains 4,824 bytes. The challenge organises the data into records of 24 characters each. Divide the file size by the record width:
4824 / 24 = 201 records
The file therefore contains 201 records of 24 characters each, stored as one continuous string.
Split the string into 24-character records
The Linux `fold` utility can wrap a long line at a specified width. Use `-w 24` to divide the data into 24-character records.
Here, this command performs three operations:
- `fold -w 24` wraps the continuous string into records that are 24 characters wide.
- `nl` numbers the resulting records.
- `head` displays the first ten records by default.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ fold -w 24 secret_bundle/STEGO_KEY_368.txt | nl -ba | head -10
1 RUYAWEYASEYEVUZUJUHUMEXU
2 FEDERUNABACEHUCETIHUROFU
3 QZFCUPBAFMCKZSHSKOSQURRJ
4 QAKUSUSOZIPECIZACELAQIQO
5 POLAZARUBUHAKAPEYUWETOQE
6 PAXUFABOGAZEJEQANIZEYABO
7 INXIIUTWJAYAPSJPZEALOKLV
8 UPNAHLNSIBESOLTUEBUPDNEY
9 TFOOXPJVDVYRLBYNHUPUGBQG
10 ZBBIQCDDJXIDRWEIQZUPBDGY
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The output makes the record positions easier to inspect. So, this means all the characters are stored on a single
line. We need to split this long string into smaller records first.
The challenge filename, `STEGO_KEY_368.txt`, indicates that we need to select records three, six, and
eight, in that exact order, and combine them into a single extraction key.
So, how do we do that? Remember the clue from the challenge brief? It mentions AWK.
But what exactly is AWK?
Select records 3, 6, and 8
AWK is a text-processing language commonly used on Linux. It can select specific records and process their contents. We will use it to extract records 3, 6, and 8, then join them without adding spaces or line breaks.
Run:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ KEY=$(fold -w 24 secret_bundle/STEGO_KEY_368.txt | awk 'NR==3 || NR==6 || NR==8 {printf "%s", $0}')
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The command works as follows:
- `fold -w 24` splits the original string into 24-character records.
- `awk` checks the record number stored in `NR`.
- The condition selects records 3, 6, and 8.
- `printf "%s", $0` prints each selected record without adding a newline.
- `KEY=$(...)` stores the combined output in a shell variable named `KEY`.
Check the key length:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ echo ${#KEY}
72
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The result is 72. That is expected because we selected three records, each containing 24 characters:
3 × 24 = 72 characters
Display the resulting value if you need to inspect it:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ echo "$KEY"
QZFCUPBAFMCKZSHSKOSQURRJPAXUFABOGAZEJEQANIZEYABOUPNAHLNSIBESOLTUEBUPDNEY
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
This 72-character string is the extraction key for `qtbl.py`. It isn't the final AES key. The Python script uses it to recover the AES key hidden inside the JPEG image.
Extract the AES Key from the JPEG Image
We now have the two required inputs:
- The recovered JPEG image: `carved/nothingtoseehere.jpg`
- The 72-character extraction key stored in `$KEY`
Run the extraction operation:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ python secret_bundle/qtbl.py extract -i carved/nothingtoseehere.jpg -o aeskey.bin -k "$KEY"
[+] Extracted 32 bytes to aeskey.bin
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ ls -al
total 316
drwxrwxr-x 6 kali kali 4096 Nov 6 13:28 .
drwxrwxr-x 6 kali kali 4096 Oct 28 12:47 ..
-rw-rw-r-- 1 kali kali 32 Nov 6 13:28 aeskey.bin
-rw-rw-r-- 1 kali kali 2452 Oct 28 12:47 BRIEF.md
drwxrwxr-x 2 kali kali 4096 Nov 6 12:59 carved
drwxrwxr-x 2 kali kali 4096 Nov 6 12:46 decoy
-rw-rw-r-- 1 kali kali 277078 Oct 28 12:47 Honey.jpeg
drwxrwxr-x 2 kali kali 4096 Nov 4 10:25 _Honey.jpeg.extracted
drwxrwxr-x 2 kali kali 4096 Nov 6 13:05 secret_bundle
-rw-rw-r-- 1 kali kali 10783 Nov 6 13:04 secret_bundle.bin
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The script processes the JPEG quantization tables and writes the recovered bytes to `aeskey.bin`. At this point, we have the AES key file and the IV file. Before attempting decryption, verify their lengths:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ wc -c aeskey.bin secret_bundle/iv.bin
32 aeskey.bin
16 secret_bundle/iv.bin
48 total
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The expected sizes are:
- `aeskey.bin`: 32 bytes, which represents a 256-bit AES key.
- `secret_bundle/iv.bin`: 16 bytes, which represents a 128-bit IV.
You can also inspect both files in hexadecimal form:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ xxd aeskey.bin
00000000: a1f8 8de3 a19d 3b74 cb5b cb39 8630 1cf0 ......;t.[.9.0..
00000010: 1469 9323 862a 0880 2998 0527 e334 2160 .i.#.*..)..'.4!`
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ xxd secret_bundle/iv.bin
00000000: 6e21 20b8 9a2a ba5f 75e0 b99d dc32 fba1 n! ..*._u....2..
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The key and IV are binary data. OpenSSL's `-K` and `-iv` options expect hexadecimal strings, so we must convert the contents before using them.
Convert the AES Key and IV to Hexadecimal
Use `xxd -p` to print each binary file as a plain hexadecimal string. The `tr` command removes newline characters, and command substitution stores the result in a shell variable.
Create the key variable:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ KEY_HEX=$(xxd -p aeskey.bin | tr -d '\n')
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Create the IV variable:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ IV_HEX=$(xxd -p secret_bundle/iv.bin | tr -d '\n')
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Check the length of the hexadecimal key:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ echo "AES key length: ${#KEY_HEX} hex characters"
AES key length: 64 hex characters
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The result should be 64 hexadecimal characters. Each hexadecimal character represents four bits, so 64 characters represent 256 bits, or 32 bytes. That is the expected size of an AES-256 key.
Now check the IV:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ echo "IV length: ${#IV_HEX} hex characters"
IV length: 32 hex characters
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The result should be 32 hexadecimal characters, representing 128 bits, or 16 bytes. That is the expected size of an AES-CBC initialization vector.
These length checks help catch common errors, such as extracting an incomplete key or accidentally including extra characters.
Decrypt `passwords.enc`
We now have the AES key and IV required to decrypt `secret_bundle/passwords.enc`.
Because we are supplying the raw key and IV directly, we do not need password-based key derivation with `-pbkdf2`. Use OpenSSL's `-K` option for the hexadecimal key and `-iv` for the hexadecimal initialization vector:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ openssl enc -d -aes-256-cbc -in secret_bundle/passwords.enc -out passwords.txt -K "$KEY_HEX" -iv "$IV_HEX"
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Identify the output:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ ls -al
total 324
drwxrwxr-x 6 kali kali 4096 Nov 9 06:33 .
drwxrwxr-x 6 kali kali 4096 Oct 28 12:47 ..
-rw-rw-r-- 1 kali kali 32 Nov 6 13:28 aeskey.bin
-rw-rw-r-- 1 kali kali 2452 Oct 28 12:47 BRIEF.md
drwxrwxr-x 2 kali kali 4096 Nov 6 12:59 carved
drwxrwxr-x 2 kali kali 4096 Nov 6 12:46 decoy
-rw-rw-r-- 1 kali kali 277078 Oct 28 12:47 Honey.jpeg
drwxrwxr-x 2 kali kali 4096 Nov 4 10:25 _Honey.jpeg.extracted
-rw-rw-r-- 1 kali kali 4958 Nov 9 06:33 passwords.txt
drwxrwxr-x 2 kali kali 4096 Nov 6 13:05 secret_bundle
-rw-rw-r-- 1 kali kali 10783 Nov 6 13:04 secret_bundle.bin
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
But what exactly did we recover? Let's identify the output
using the file command.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ file passwords.txt
passwords.txt: ASCII text, with very long lines (4824)
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The file is identified as ASCII text with very long lines.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ cat passwords.txt
secret.enc
DesertStorm#82pLm
decoy_random.enc
P@$$w0rd1!
payload.enc
L35f#t8w2&(X$MK8:`SPXa=WV{F%L1G7u8@[>6yI;<N=]=e5#5
STEGO_KEY_386
RUYAWEYASEYEVUZUJUHUMEXUFEDERUNABACEHUCETIHUROFUQZFCUPBAFMCKZSHSKOSQURRJQAKUSUSOZIPECIZACELAQIQOPOLAZARUBUHAKAPEYUWETOQEPAXUFABOGAZEJEQANIZEYABOINXIIUTWJAYAPSJPZEALOKLVUPNAHLNSIBESOLTUEBUPDNEYTFOOXPJVDVYRLBYNHUPUGBQGZBBIQCDDJXIDRWEIQZUPBDGYBWQYSYFJJOWHTWTSJJHTEGUZPUQIJUTITOTAZUCAWUNEMUQIYPQRMRANBXCFJKULNFUOZEZEMSHRIXYELIUIRYTPYXONAWTICQXCJRYRYEBPFVWYHEJWGDEHLUFIMIPEJUQOLUVEFILIWIZUWEBAGUBINOXOZOCODAYEDUSIXAQOMIQUBIJOSAWOMUDORUVIJJIQOROHXQBBOZWFWGEBGMAIMINEGIKASONOCOJEYUMOHAZITQYYZWMCWQXZVEEHCXBXLPYSIFAPESHCPLTKFRFMPURMCEAKTQEFCGKKXQVSYRPKNQQMWBRUMAQOCOCEROPOYEHAHIGOWOJOPFWGTNOTSFCIQAHZVGMJTVPYTATIHUXIKEBIPEKIXOXUZERUGAVARONIBODACIBOLOFEBUXOHUFOWIRUMEDOXEBIFALOGOBEQQOXINNOOKECJDSSDTIYZQDUCDGTJTXSZJTURUPFDKDPKCBPGADEZUZUKUVEHABOBUMEZAHUZAFUDAHAYIMURAMIWEHIMOCATOKUHOXAFUJAJOYINAXUSUMAVUSIFIWICENEHILEYUXAKIZIMOXUROSOYUZIFANESAFUZIRUFAPEPATIRUYENOKUSEFOJIHALYKNEWEBKXVPUXUOKOUVILPDRAGONUZUXOBUSUZUNOPAJOFEBIFULOYIHARAVIFEKABIKOWONOODMGEWBGALLJINDWOSOHTTCERIJUGOHEPEBEZOXUGUHAWIZIPLREVGNRBDBYLKJWDHHFRWPISOKADEWIQUHUYEVUBEQEJISOKOREZENIHEFUDIGALAMEJETHHCRULHAXMZEPUMKZWENRTOXDIYMAIGJCBQDLDZAKINSCJUXOWUKAKAVEVORIXEPABAHUXOABBYWBPBEKMEGQETVLKHDXAKJQXZMJRPUUNRDSOJSCNJTLHNADRWFAPUMXFQQIFAZOVQORJRRMNPAWPENZVOJNPGRNNXHKEGWAGUFEWISIQUQANEJACOSAHEADAFMGXDLVEUKHQMQEIVUIPHWJAFMWEXPQEAKIGTUBQCLZNUMALEBAHEFAWIRUNEJARISUNUCNJWSAWAUFIGGWMPRQVIMHDZVICEFOWOXUXIZAVECOTOMAGAHMZLUYIMMUZVHOGEZFXMDJKUVIFESEMOJEWITINUJARARUNEXCRVWPHNSSJSQMSBHHDRSOIBEOQGCYQPPCDKJQUORQYUKLSQKVYOBLXNSVWIRNZJCAZNSJZQFBVRXGCSEIFWJCBLMXRZADSUBOJUSEHEBUFUNURUXEFUTEHEVRGYPUZEHQUKSZYTIYDJBZCZZMBXMPPENIJTSSMBRKNXIJTMFUJIDECUWURIGABOSOBALODOTMZFUQFOCHQCHCHYBSBHDRYPEJOXOTVTLKSNMMPLLKLPIAXJLIVETUFEBUPERITEBABATALAWYTBCQBWZYLKUSVZGYWRAWLYWUFIQUJIFEVUQAZOKOFUFIJAFUCIBUCEPOBUBENETEPEXEYEHEKOJEXEHIJOROLOJUJUJAYUCRPUZYNPSEVYVFBEVQBEQZOVTAJEYIRALUQAFEPIVOZOWANEZZJWLJJTQUMDOCRSFBQARLFTLFRFMNRYGBDPSBNZOFVPALHHOJYBRMNUOLKSXADWYLITHIRNLNQVNJQTYHBYEXVATDGVYJGXQZLLQTBWWSBPJMVSYTOQELLUZAPKIDXQKHSYOIPLPIQLKRCXKUGOBOVAXIBOLISAQIDUNEXUQICONIWIPEYUZOYEDAWUYACULXAMZANQUWCJTASKFIHIFTPVTAWIVAHETETIPAQIRINAHAVIDUPWGETBPCNDOUCYJWHRPZIYQANETJFUEZTVQPQKJOBPUXPLBIVOCIBOYUHECOPEHIGETEVUPXIUYWXOQDTWFFISXOOWHVLFCNMASXMARPXGJOIVYTFTIBNLKONXIYTDWUFPTYSZEJKVBVGZFOVCWEEKDXSQOKIINTAFVDOPIEJFADVXVYGVPWYLMKVQEJBGZEVRGREZACSSOAMFFAEFHQXPTNCYDAUXQCKAXZBMSKNSFQIRPUMCUQKXVZXGSUNETPYYKIDELGCLDRODWTYOZGKBBNVSOCGPSAJCQGTKQTLUMPOASFVUFOGASBVUUJHPCITNDWIDARJJNATFRYEMLDQXTEZHDSYRRYCSRTTBITPIVEDPKNSSFEASDBJTNBULMITIUXLSJWSHWHPZFNACEGZYSOWOGODEPIDUCOJIWIGAZAVIWKGOEFYYHDDWJFSNZUXKVJLTBOYOVATABETOJUHOBIYOLUJUTPZFFDYKSKDLBWXTVNPUPWQYBAJEYADERELERAFAHATEZEYAJZNHHWYMLTDBIVXSWFKTDXDDZEMUJOQILAMAMOWUVUQIBOHOXAGETIVOFUDEYOLOZIGEXAPAOKRBBUHKFJJQJHCODIGKLZXKJEGOGUBAGEQUMUPUQAVIWUFIDBUETQLJLQRNFAHEXXIZRBVIAKCISZBMWPXLCUAGXCJBQTDVPIXIBIYOPUYACASETEXEFAJEBOTURAZAMALAROLUPOJERADATNUNDFQWPBCCXHOJASEAUSSIHEREPAMESOYACUDAVOQETIFINOWOKIWAYUDEQOYEROFUVAFUEWWSCCZHZQRIOYEJCJHBIGLFRXYBMGBKPJZDTXVFVFPHPGTCRUSESENEJIVUHELOJELABUMOLMUHOKAIRHWFFVZMVQKEWCEYRQSJAPOQQMXOUWVKKKWSUXBSPBYOJTOVUOWMULEIDWRRBSZHPUPATAGAYOWAGAQEXOFOJEGUWEUZXCEJHWDEQZWRJKZVGLBWSUANTTRIHABKTCTPIEUXENUQJCQDIWETUXACFCLGJABGXCFWWPLJCHAIQLPYGULJLREUHYKJMIZEJAVOGAVUHIBIZAKIFASIWCSJOVVBXLXRXKRROJLNXGCFIRLEEFSISSVKQYEPUPXYUTRJZOSAYUCALOMOMACIJUFUVULOWLMMPKSAOMFELVVJRMHBPUOCEKGAKLJQODLDEKJIFQQVDRBVSOSOMUGUNOYEQEWUXOCOVEVAFEHOBAJITIVADUNOHIWUKOXUKZPPMBFZNQRIAHFSQVUTCMNUOYORUNBZHATFTDXGKVAWQNAYQTOVPCENXXCOTJAHJTBNUZZAYLWHNKLKYKDBMKAEQBYDHCCNLEDEDUQAPUTUYOLOROPEXAFUIXTAWYELRFCQDESNXHKAENACRALOXIPUHESUROQAFOVAXEKOEPACVWNUZDNGMEQAQRZFANKQWUXOTEZAQAFADOXAYINIDIJANNPDYMBVQEZMPJQWOAXAVBCKPAZJALFQMVLKUYKVYQOZFJFBSUQUZAGICEDAGEHAPOLUHANAGXHVBWBTLXQVMETJCYOKHYWNNVNIGIOLNDJIVTZWZOJRFCTNQAFAKAHUVACIVAVAQAJELUSIVMLUDVYOIKZNRWNNGFYYXRGVTUHITUJODARAXESEROZOVISACIKEQUFEMAJOCIJOCOSIREBUHECIDUBUTUFUKEKIRIVUGIMEVODMNYEXVSRZEUEBZMAFBYNBRSOKJIUJRSWEWEDKVKBFSJRLVNMXMVKKRFWGVWZQJGQUUACEXKBMZJJTHFQDYMONYUAZHBGTECUABCAIYSANUSIUGFDWDYTXSEHAHIXISINOMORICUFETIKEORXFKNQTRXUVWHLOSDWXEAZVWVHUYILWTUNQPZDABWYBMRWERAFUKIZAPOGIJIKURIWUTIFEARLHGXOLVISYCRPVYMOVOBRKAOOHQGCBNIWMIXKTVTCTUYRYPPZHVLQWBXIQKIJQVWSWGYCHPOQAVIVISICURAFUBACEPIQAMALOCOFOLITUFUFAHIRELUTOCRHWRPVLZOMWJPWGMTKAHSXNGGRIEKMWEVGZMXQTYQJFZCRYXTIACRDUHNTQEIDOKDQSPAOOHMSLNZYWKRGTIBJMWYDTHUCATEXOYOSAHOZAYOCOVEGOWACIXVIGUMPTAICPNJHQHLJKIVRILMPSTEPUKGEVMMUZUXSEXNJKKDWCAYLFBQMRDSEQDDXTQJHUIXBRRPRCFZYYVRUSROPDHYCCXNMJDAAGSRFBBEKGABDWEURTHUHEPIXUVIKEFOQOJIZEVAQEXOCEYUGUHEXITEDESIROCOFITQREHJKSIMXLPWWNYBYMXJXFROVIZAKAYIGABIBEHIFOMUQEBCOKCPRJFKPBUHVBYXWDSGKDYBWRCMXMKCWSITOFMILANIITNGCDFPMFUIQKHZFSUCBDTGCHBUNICIMEVAVONOGEMEKUFIYEXLGXCSUJRRRKIIWDIXWTISWWCMJJMXDMFZUNMTDSMUJTLNRVTUNEJATUDEMIQAKUTILIRIWOWUYUBOFIJISOTACUPAQUGIQOWOCFPWFHLNAKZHRDRTOGHMPBWDBQBENHRCYDXBETGPNFSAJMFPPDTPJTUWTHUHLANNEJTQJJDUCOBITENAVINUXOQETALIWALJYTKHHDFBBIEFGRILLMNVNABEOSIDEGBQFCOKNHTFLNRDXJCOFAXUPASIJAXOSIYENIXEJE
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
While inspecting the first six lines, one particular entry caught my eye. It looks interesting and may contain an important clue.
Instead, I'll use the head command with the -n option to
display only the first six lines.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ head -n 6 passwords.txt
secret.enc
DesertStorm#82pLm
decoy_random.enc
P@$$w0rd1!
payload.enc
L35f#t8w2&(X$MK8:`SPXa=WV{F%L1G7u8@[>6yI;<N=]=e5#5
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The first entries show a filename and its corresponding password. The first pair refers to `secret.enc`, which we have already decrypted.
secret.enc
DesertStorm#82pLm
The remaining entries provide passwords for other encrypted files from the challenge.
One of the entries is associated with `decoy_random.enc`. Decrypting that file produces data that does not immediately identify itself as a familiar file format. We can inspect it, but it does not reveal a useful route to the flag.
For example, after decrypting the decoy with its corresponding password, inspect the result with:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ file decoy_random.bin
decoy_random.bin: data
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Let's use xxd:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ xxd -l 64 decoy_random.bin
00000000: 47df 1b0b cae3 a340 047d fde7 eed9 a59e G......@.}......
00000010: 35fd ef4a fa37 1c5f 17be 32b6 81e2 7df8 5..J.7._..2...}.
00000020: 293e 0e9e 6179 2dd2 e3b1 8123 640c 65cb )>..ay-....#d.e.
00000030: 5917 cf28 8587 f586 1338 8104 0b00 f5cc Y..(.....8......
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ strings -n 6 decoy_random.bin | head -30
B`JN0K(jr
]1^4)]A;
"0?rY%
JH0L+cH
?~|fWT/
{N@fU(
:2A0t7
S%3I.b
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ binwalk decoy_random.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
In this challenge, the output is reported only as generic data. The initial bytes do not show a familiar file signature, and Binwalk does not identify another embedded structure. We can set this result aside and continue with the password entry associated with `payload.enc`.
Extract the Password for `payload.enc`
The sixth line of `passwords.txt` contains the password associated with `payload.enc`. Instead of typing a long password manually, save that line to a separate file.
Use `sed`:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ sed -n '6p' passwords.txt > pw.txt
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Here:
- `-n` suppresses the normal output of `sed`.
- `6p` prints only line 6.
- `>` redirects the selected line into `pw.txt`.
Check the saved value:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ cat -A pw.txt
L35f#t8w2&(X$MK8:`SPXa=WV{F%L1G7u8@[>6yI;<N=]=e5#5$
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The `$` displayed at the end of the line represents the newline character. It is not part of the password. When OpenSSL reads the password from a file, it handles the line ending as expected.
Using a password file also avoids several shell interpretation problems that can occur when a password contains characters such as `$`, `&`, semicolons, or backticks.
Decrypt `payload.enc`
We now have the encrypted file and its corresponding password. Use OpenSSL with AES-256-CBC and PBKDF2, and tell it to read the password from `pw.txt`:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ openssl enc -d -aes-256-cbc -pbkdf2 -pass file:pw.txt -in carved/payload.enc -out payload.bin
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Identify the decrypted output:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ file payload.bin
payload.bin: Zip archive data, made by v3.0 UNIX, extract using at least v1.0, last modified Sep 28 2025 19:13:44, uncompressed size 55, method=store
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
This time, the file utility identifies the output as a ZIP archive. This is a promising result: the decrypted payload contains another archive that may hold the flag.
Test and extract the final archive
Before extracting it, test the ZIP archive's integrity:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ unzip -t payload.bin
Archive: payload.bin
testing: flag.txt OK
No errors detected in compressed data of payload.bin.
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The test completes without reporting an error. Extract the archive into a separate directory:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ unzip payload.bin -d payload
Archive: payload.bin
extracting: payload/flag.txt
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
List the extracted files:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ ls -al payload
total 12
drwxrwxr-x 2 kali kali 4096 Nov 9 06:50 .
drwxrwxr-x 7 kali kali 4096 Nov 9 06:50 ..
-rw-r--r-- 1 kali kali 55 Sep 28 2025 flag.txt
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
Locate the flag file in the output and display it. Use the actual filename shown by your extraction results:
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$ cat payload/flag.txt
Flag{Y0u haVe EnCouNTeR3d a w!Ld s1eEP p@RA1y$!S DEm0n}
┌──(kali㉿kali)-[~/…/steganography-ctf/participant/challenges/03-steganography-lvl-3]
└─$
The flag confirms that we have successfully completed Challenge 3.
What We Learned from Challenge 3
This challenge required more than finding a hidden file inside an image. We had to combine several forensic and steganographic techniques:
- File identification: We used `file` to confirm the target's actual format instead of relying on its extension.
- Metadata inspection: We used ExifTool to examine the JPEG's metadata.
- Signature analysis: We used Binwalk to identify embedded encrypted sections, a ZIP archive, and another JPEG image.
- Manual file carving: We used `xxd` to verify boundaries and `dd` to extract exact byte ranges from the original image.
- Password-based decryption: We used a password pattern from the challenge description to decrypt the first OpenSSL-encrypted section.
- JPEG steganography: We examined the supplied Python script and used it to recover an AES key from the image's quantization tables.
- Text processing: We used `fold` and AWK to construct the extraction key from selected records in `STEGO_KEY_368.txt`.
- Raw-key decryption: We converted the recovered binary AES key and IV into hexadecimal strings and used them to decrypt `passwords.enc`.
- Archive analysis: We decrypted `payload.enc`, tested the resulting ZIP archive, and extracted the flag.
The central lesson is to work from evidence. A signature can tell us where a structure begins, but we must verify its boundaries before carving it. A file that the system reports as generic data is not necessarily useless; it may be raw ciphertext or a format without a recognisable header. Finally, encrypted data may require several separate clues—a password pattern, an embedded key, and an initialization vector—before it can be recovered.
That completes the walkthrough for Steganography CTF Challenge 3. Share your questions or observations in the comments, especially if your offsets or extraction results differ from those shown here.