Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
CryptoMedium

Cute

CTF writeup for Cute from Vianu CTF

//Cute

>Summary

We are given a seemingly innocent JPEG. The trick is that the image has extra bytes appended that form an encrypted ZIP, containing a file with an RSA private key (DER, Base64-wrapped) and a Base64 ciphertext. Once we crack the ZIP password, we can convert the DER key to PEM and decrypt the ciphertext to obtain the flag.

Flag: CTF{d0_y0u_l1k3_RSA_4nd_st3g4n0}


>Files Provided

  • cat.jpg
  • cat.jpg:Zone.Identifier

Note about Zone.Identifier

Zone.Identifier is Windows “Mark of the Web” metadata sometimes preserved when files are downloaded. In many CTFs it’s a red herring; here it only contains:

[ZoneTransfer]

So the actual work is in the JPEG.

cat.jpg

cat.jpg


>Mindset / How I got the idea

When a challenge says “something is hidden inside an image”, the most common starting ideas are:

  1. Metadata / EXIF (quick to check)
  2. Strings (sometimes a literal flag is present)
  3. Steganography tools like steghide
  4. File carving / appended payload: images can carry extra data after the normal image end marker, and many tools still open them

Because this is in the Crypto category (not pure Stego), I expected:

  • an encrypted blob (ZIP, AES, etc.)
  • or an encoded ciphertext/key hidden in/after the image

So my path was: check quick hints → then scan for embedded file signatures.


>Local Success (Full Walkthrough)

1) Recon: file type and basic inspection

bash

ls -la

file cat.jpg

strings -n 6 cat.jpg | head

exiftool cat.jpg | head

This confirms it’s a normal JPEG and doesn’t immediately reveal a flag in plain text.

2) Find hidden payload (embedded ZIP)

The high-signal move is to scan the JPEG for embedded file signatures.

bash

binwalk -e cat.jpg

Output shows a ZIP embedded near the end:

  • “Zip archive data … name: secret.txt”

Binwalk auto-extraction creates:

  • _cat.jpg.extracted/630D9.zip

At this point we have a ZIP inside a JPEG.

3) The ZIP is password-protected

Trying to read it directly prompts for a password:

bash

cd _cat.jpg.extracted

unzip -p 630D9.zip secret.txt

So we need the ZIP password.

4) Crack the ZIP password

Because the ZIP is small and contains a single file, a dictionary attack is fast.

Using fcrackzip with the system dictionary:

bash

fcrackzip -D -p /usr/share/dict/words -u -v 630D9.zip

This reveals:

  • Password: dragon

5) Extract secret.txt

bash

unzip -P dragon 630D9.zip

cat secret.txt

The file contains:

  • Private key: followed by Base64 (wrapped across multiple lines)
  • One final Base64 string (ciphertext)

This is a strong crypto tell: RSA private key + ciphertext.

6) Convert key + decrypt ciphertext (RSA)

The private key is in DER format (Base64-wrapped). We decode it to key.der, convert it to PEM, then decrypt the ciphertext with openssl pkeyutl.

The manual commands I used (same logic as in the solver script):

bash

# Parse/assemble Base64 properly, produce key.der and ct.bin

python3 - <<'PY'

import base64, pathlib

lines = [ln.rstrip() for ln in pathlib.Path('secret.txt').read_text().splitlines() if ln.strip()]

assert lines[0].startswith('Private key:')

key_parts = [lines[0].split('Private key:',1)[1].strip()] + [ln.strip() for ln in lines[1:-1]]

key_b64 = ''.join(key_parts)

ct_b64 = lines[-1].strip()

pathlib.Path('key.der').write_bytes(base64.b64decode(key_b64))

pathlib.Path('ct.bin').write_bytes(base64.b64decode(ct_b64))

print('key.der bytes', len(pathlib.Path('key.der').read_bytes()))

print('ct.bin bytes', len(pathlib.Path('ct.bin').read_bytes()))

PY

# DER -> PEM

openssl pkey -inform DER -in key.der -out key.pem

# Decrypt (try PKCS#1 v1.5)

openssl pkeyutl -decrypt -inkey key.pem -in ct.bin -pkeyopt rsa_padding_mode:pkcs1

This prints the flag:

CTF{d0_y0u_l1k3_RSA_4nd_st3g4n0}


>Solver Code (Reproducible)

solve.py

This repository includes a full solver script:

  • solve.py
python
#!/usr/bin/env python3

"""Cute (Crypto) solver.

Pipeline (fully local, no guessing):

1) Carve appended ZIP from the JPEG (search for PK\\x03\\x04 signature).

2) Crack the ZIP password using a dictionary (defaults to /usr/share/dict/words).

3) Extract secret.txt from the ZIP.

4) Parse base64-wrapped RSA private key (DER) + base64 ciphertext.

5) Use OpenSSL to convert DER->PEM and decrypt the ciphertext.

This avoids relying on binwalk/fcrackzip/steghide availability.

Requires: python3 + openssl.

"""

from __future__ import annotations

import argparse

import base64

import os

import subprocess

import sys

import zipfile

from pathlib import Path

from typing import Iterable, Optional, Tuple

ZIP_LOCAL_SIG = b"PK\\x03\\x04"

def carve_zip_from_jpeg(jpeg_path: Path, out_zip: Path) -> int:

    data = jpeg_path.read_bytes()

    off = data.find(ZIP_LOCAL_SIG)

    if off < 0:

        raise SystemExit("[-] No ZIP signature (PK\\\\x03\\\\x04) found in JPEG")

    out_zip.write_bytes(data[off:])

    return off

def iter_wordlist(wordlist_path: Path) -> Iterable[str]:

    # Tolerate weird encodings; strip CRLF.

    with wordlist_path.open("rb") as f:

        for raw in f:

            word = raw.strip().decode("utf-8", errors="ignore")

            if word:

                yield word

def crack_zip_password(zip_path: Path, member_name: str, wordlist_path: Path, limit: Optional[int] = None) -> str:

    with zipfile.ZipFile(zip_path) as z:

        if member_name not in z.namelist():

            raise SystemExit(f"[-] ZIP does not contain {member_name!r}")

        for idx, word in enumerate(iter_wordlist(wordlist_path), start=1):

            if limit is not None and idx > limit:

                break

            try:

                z.read(member_name, pwd=word.encode())

                return word

            except RuntimeError:

                # Bad password

                continue

            except zipfile.BadZipFile:

                raise

            except Exception:

                continue

    raise SystemExit("[-] Password not found in provided wordlist")

def extract_member(zip_path: Path, member_name: str, password: str, out_path: Path) -> None:

    with zipfile.ZipFile(zip_path) as z:

        data = z.read(member_name, pwd=password.encode())

    out_path.write_bytes(data)

def parse_secret_txt(secret_path: Path) -> Tuple[bytes, bytes]:

    # File format observed:

    #   Private key:<base64...>

    #   (maybe wrapped base64 continuation lines)

    #   <base64 ciphertext>

    lines = [ln.strip() for ln in secret_path.read_text(errors="ignore").splitlines() if ln.strip()]

    if not lines or not lines[0].startswith("Private key:"):

        raise SystemExit("[-] secret.txt format unexpected (missing 'Private key:')")

    # Everything except the last non-empty line belongs to the key base64.

    key_first = lines[0].split("Private key:", 1)[1].strip()

    key_cont = [ln for ln in lines[1:-1]]

    key_b64 = "".join([key_first] + key_cont)

    ct_b64 = lines[-1]

    try:

        key_der = base64.b64decode(key_b64)

        ct = base64.b64decode(ct_b64)

    except Exception as e:

        raise SystemExit(f"[-] Base64 decode failed: {e}")

    return key_der, ct

def run(cmd: list[str], *, input_bytes: bytes | None = None) -> bytes:

    p = subprocess.run(cmd, input=input_bytes, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    if p.returncode != 0:

        raise SystemExit(

            "[-] Command failed:\\n"

            + "    "

            + " ".join(cmd)

            + "\\n"

            + p.stderr.decode(errors="ignore")

        )

    return p.stdout

def rsa_decrypt_with_openssl(key_der: bytes, ciphertext: bytes) -> bytes:

    # Convert DER key to PEM (openssl pkey).

    # Then decrypt ciphertext (openssl pkeyutl).

    with Path("key.der").open("wb") as f:

        f.write(key_der)

    run(["openssl", "pkey", "-inform", "DER", "-in", "key.der", "-out", "key.pem"])

    Path("ct.bin").write_bytes(ciphertext)

    # Try PKCS#1 v1.5 first (typical for CTF RSA toys), then fallback.

    try:

        return run([

            "openssl",

            "pkeyutl",

            "-decrypt",

            "-inkey",

            "key.pem",

            "-in",

            "ct.bin",

            "-pkeyopt",

            "rsa_padding_mode:pkcs1",

        ])

    except SystemExit:

        return run(["openssl", "pkeyutl", "-decrypt", "-inkey", "key.pem", "-in", "ct.bin"])

def main() -> None:

    ap = argparse.ArgumentParser(description="Solve Cute (Crypto) challenge")

    ap.add_argument("--image", default="cat.jpg", help="Path to the JPEG")

    ap.add_argument("--wordlist", default="/usr/share/dict/words", help="Dictionary for ZIP password cracking")

    ap.add_argument("--limit", type=int, default=None, help="Max words to try (debug)")

    ap.add_argument("--workdir", default="solve_out", help="Output directory")

    args = ap.parse_args()

    image_path = Path(args.image)

    wordlist_path = Path(args.wordlist)

    workdir = Path(args.workdir)

    workdir.mkdir(parents=True, exist_ok=True)

    if not image_path.exists():

        raise SystemExit(f"[-] Missing image: {image_path}")

    if not wordlist_path.exists():

        raise SystemExit(f"[-] Missing wordlist: {wordlist_path}")

    zip_path = workdir / "embedded.zip"

    offset = carve_zip_from_jpeg(image_path, zip_path)

    print(f"[+] Carved ZIP from {image_path} at offset {offset} -> {zip_path}")

    member = "secret.txt"

    password = crack_zip_password(zip_path, member, wordlist_path, limit=args.limit)

    print(f"[+] ZIP password found: {password}")

    secret_path = workdir / member

    extract_member(zip_path, member, password, secret_path)

    print(f"[+] Extracted {member} -> {secret_path}")

    key_der, ct = parse_secret_txt(secret_path)

    print(f"[+] Parsed key DER bytes={len(key_der)} ciphertext bytes={len(ct)}")

    # Do crypto work in workdir so we don't clutter the folder.

    cwd = os.getcwd()

    try:

        os.chdir(workdir)

        pt = rsa_decrypt_with_openssl(key_der, ct)

    finally:

        os.chdir(cwd)

    try:

        flag = pt.decode().strip()

    except Exception:

        flag = repr(pt)

    print(f"[+] Plaintext: {flag}")

if __name__ == "__main__":

    main()

It:

  • carves the ZIP from the JPEG by searching for PK\\x03\\x04
  • cracks the ZIP password with /usr/share/dict/words
  • extracts secret.txt
  • Base64-decodes the DER key and ciphertext
  • uses OpenSSL to decrypt and prints the flag

Run:

bash

python3 solve.py --image cat.jpg

Optional:

bash

python3 solve.py --image cat.jpg --wordlist /usr/share/dict/words

python3 solve.py --image cat.jpg --limit 200000  # debug

>Why this works (short technical explanation)

  • Appended ZIP inside JPEG: JPEG decoders typically ignore trailing bytes after the image ends. That allows attackers/CTF authors to append a ZIP without breaking image viewing.
  • Password-protected ZIP: forces a cracking step before you can reach the crypto material.
  • RSA private key + ciphertext: once you have the private key, RSA decryption is straightforward with OpenSSL.

>References


>Final Answer

CTF{d0_y0u_l1k3_RSA_4nd_st3g4n0}