This level is hybrid of Programming/Stego missions, The main purpose
being: Get the data from the image, and get the answer from that data.
The data itself is encoded and encrypted multiple times, in the end is a
10 character password. The password consists of two characters, one
upper case and one lower case, repeating in random order. Example:
XyyXyXXyXy In order to get this data you must brute-force the encrypted
hashes you get in each step. To do this within the time limit you must
write a smart brute forcing method, good luck. Save this image to get
started.
im = Image.open('image1.png').convert('RGB') w, h = im.size px = im.load() cols = collections.Counter(px[x, y] for y inrange(h) for x inrange(w)) print(im.mode, im.size) for c, n in cols.most_common(6): print(c, n) print('unique colors:', len(cols)) print('R values', set(c[0] for c in cols), 'G', set(c[1] for c in cols))
B 0 printable! UUUUUUUUUUUUUUUUV.................UUUUUUUUUUUUUUUUj................UU... B 1 printable! UUUUUUUUUV.................UUUUUUUUUUUUUUUUj................UU...
digest = 'e39654b46edc59284c40bad96887ec541e476f96d6b7e81b3dff92601eebcfec' t0 = time.time() n = 0 for lo in string.ascii_lowercase: for up in string.ascii_uppercase: for mask inrange(1 << 10): pw = ''.join(up if (mask >> i) & 1else lo for i inrange(10)) n += 1 if hashlib.sha256(pw.encode()).hexdigest() == digest: print('found', pw, 'after', n, 'hashes in', round(time.time() - t0, 2), 's') raise SystemExit
#!/usr/bin/env python3 """HackThisSite Programming mission 10 -- Automated Steganography (45 s limit). Pipeline (all in one process, one instance fetch): 1. GET https://www.hackthissite.org/missions/prog/10/ -> form + random image URL 2. GET .../missions/prog/10/image.php?<rand> -> 255x128 RGB PNG 3. The PNG is a 255x128 triangle-wave gradient drawn in ONE randomly chosen channel (R, G or B). The grader hides one byte per row in the *x position* of the single pixel that also carries a non-zero value in one of the other two channels: char = chr(x_of_the_off_gradient_pixel) Only 88 of the 128 rows carry a marker; concatenating those chars in row order yields the base64 string. 4. base64-decode -> 64 hex chars -> a SHA-256 digest. 5. The answer is a 10-character password built from exactly two distinct characters (one lower case, one upper case) in random order, so it is found by hashing all 26*26 pairs x 2^10 orderings and comparing to the digest (~0.7 s in CPython; no external tools needed). 6. POST the password back to .../missions/prog/10/index.php (field `solution`). Usage: cd <hts-workspace> export HTS_COOKIE='HackThisSite=...' uv run python challenges/hts-prog/10/solve.py # solve + submit uv run python challenges/hts-prog/10/solve.py --dry-run # decode only The cookie is read from HTS_COOKIE at runtime and is never written to disk. """
import argparse import base64 import hashlib import io import os import re import string import sys import time
from PIL import Image import requests
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from common import BASE, body_text, fetch_level, session, submit # noqa: E402
deffind_image_url(page_text): m = IMAGE_RE.search(page_text) ifnot m: raise SystemExit("image.php URL not found on level page (not logged in?)") return m.group(1)
defextract_base64(png_bytes): """Return (base64_string, per-row marker offsets). The instance renders a triangle-wave gradient in ONE randomly chosen channel (R, G or B) and hides one byte per row in the x-offset of the single pixel that also carries a value in another channel. Detect the gradient channel as the one with the most distinct values, then take, for every row, the x of the first pixel whose other channels are non-zero. """ im = Image.open(io.BytesIO(png_bytes)).convert("RGB") w, h = im.size raw = im.tobytes() print(f"[1/4] image: {w}x{h}{im.mode}") at = lambda x, y: raw[3 * (y * w + x):3 * (y * w + x) + 3]
spread = {c: len({at(x, y)[c] for y inrange(h) for x inrange(w)}) for c inrange(3)} grad = max(spread, key=lambda c: spread[c]) others = [c for c inrange(3) if c != grad] print(f" gradient channel: {'RGB'[grad]} (distinct values {spread})")
xs = [] for y inrange(h): hits = [x for x inrange(w) ifany(at(x, y)[c] != 0for c in others)] if hits: xs.append(hits[0]) s = "".join(chr(x) for x in xs) print(f"[2/4] rows carrying a marker: {len(xs)}/{h}") print(f" marker offsets (first 20): {xs[:20]}") print(f" layer 1 (chars from offsets): {s}") return s, xs
defdecode_layer1(b64_str): pad = b64_str + "=" * (-len(b64_str) % 4) raw = base64.b64decode(pad) digest = raw.decode("ascii").strip() print(f"[3/4] layer 2 (base64): {raw!r}") ifnot re.fullmatch(r"[0-9a-f]+", digest): raise SystemExit("decoded value is not a hex digest") print(f" layer 3 (digest): {digest} ({len(digest)} hex chars)") return digest
defcandidates(): """Every 10-char string built from one lower + one upper case letter.""" for lo in string.ascii_lowercase: for up in string.ascii_uppercase: for mask inrange(1 << 10): yield"".join(up if (mask >> i) & 1else lo for i inrange(10))
defbrute_force(digest): """Hash candidates algo-major so the common case (sha256) is ~0.5 s.""" t0 = time.time() for algo in ALGOS: tried = 0 for pw in candidates(): tried += 1 if hashlib.new(algo, pw.encode()).hexdigest() == digest: print(f"[4/4] cracked {algo} after {tried} hashes " f"in {time.time() - t0:.2f}s") return pw, algo raise SystemExit("no password matched (unexpected hash algorithm?)")
defmain(): ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true", help="decode only, do not submit") args = ap.parse_args()