Challenge 3 - Uncovering Multiple layers of Hidden Data
steganography steganography-ctf

Challenge 3 - Uncovering Multiple layers of Hidden Data

November 11, 2026 • 75 min read • 17 views • 0 discussions
Table of Contents

    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:

    • 3C3D6.zip
    • do_not_open.txt

    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:

    DesertStorm#82pLm

    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.

    Community Q&A