//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.jpgcat.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
>Mindset / How I got the idea
When a challenge says “something is hidden inside an image”, the most common starting ideas are:
- Metadata / EXIF (quick to check)
- Strings (sometimes a literal flag is present)
- Steganography tools like
steghide - 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
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.
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:
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:
fcrackzip -D -p /usr/share/dict/words -u -v 630D9.zip
This reveals:
- Password:
dragon
5) Extract secret.txt
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):
# 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
#!/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:
python3 solve.py --image cat.jpg
Optional:
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
- Binwalk (file signature scanning / extraction): https://github.com/ReFirmLabs/binwalk
- fcrackzip (ZIP password cracking): https://manpages.debian.org/fcrackzip
- OpenSSL
pkey(key conversion): https://www.openssl.org/docs/manmaster/man1/openssl-pkey.html - OpenSSL
pkeyutl(public key operations including RSA decrypt): https://www.openssl.org/docs/manmaster/man1/openssl-pkeyutl.html - ZIP file format signatures (PK headers):
- https://en.wikipedia.org/wiki/ZIP_(file_format)
>Final Answer
CTF{d0_y0u_l1k3_RSA_4nd_st3g4n0}