scriptCTF Writeup
A writeup for the scriptCTF challenges i picked among those that I managed to solve.

Secure Server1
Ooh that was long …..man.it has been quite sometime since i got my hands on CTFS but this one was cool it brought me back like i was never away.Thanks and credits to the organizers ScriptSorcerers.
lets dive to the reason we are here the challenges,this were the ones i thought were cool to share.
CRYPTOGRAPHY SECTION
This was like juicy,,,,,, easy peasy.we were given a file so extracting the file gave us two files :
``capture.pcap ,server.py
Let’s read the server.py
code first to understand how the “secure server” works.
import os
from pwn import xor
print("With the Secure Server, sharing secrets is safer than ever!")
enc = bytes.fromhex(input("Enter the secret, XORed by your key (in hex): ").strip())
key = os.urandom(32)
enc2 = xor(enc,key).hex()
print(f"Double encrypted secret (in hex): {enc2}")
dec = bytes.fromhex(input("XOR the above with your key again (in hex): ").strip())
secret = xor(dec,key)
print("Secret received!")
Hereโs a breakdown of the challenge based on the above extracted files:
1. Understanding the Server Code (server.py
)
The server implements a double XOR encryption process with a random key:
Step 1: User submits their secret already XORed with their own key
Step 2: Server applies an additional random 32-byte key key
Step 3: User is supposed to XOR enc2 with their key again
Step 4: Server recovers the secret secret
๐ Key pointsi considered:
- The server never directly sees the plaintext; it only works with XORed data.
- The server applies a second layer of XOR with a random key before sending data back.
- To retrieve the plaintext, XOR must be undone in the correct order.
2. What We Have (capture.pcap
)
Since we have a packet capture, it likely contains:
- The initial XORed secret from John Doe (
enc
). - The serverโs response (
enc2
). - Possibly John Doeโs response (
dec
).
With all three, we can reconstruct the original plaintext.
3. XOR Recovery Logic
Recall XOR properties:
- AโBโB=AA \oplus B \oplus B = AAโBโB=A
- If we know two stages of XOR, we can always derive the third. From server code:
- `enc = secret โ user_key
enc2 = enc โ server_key
- User sends back
dec = enc2 โ user_key
- Server recovers
secret = dec โ server_key
From traffic:
- If we extract enc, enc2, and dec from the pcap, we can solve for the secret without knowing the keys.
4. Plan to Recover the Flag
- Parse the
.pcap
to extract hex payloads. - Identify which are
enc
,enc2
, anddec
. - Use the XOR relationships to recover the original plaintext secret (The flag).
Bydhaway i had my buddy chat-gpt to craft the code for easy solving.
Why this works
XOR is linear and self-inverse: A โ B โ B = A
. If an attacker sees two different XOR โviewsโ of the same underlying data, they can eliminate unknown keys by XORing the views together. Here, the protocol unintentionally exposes enough material (enc
, enc2
, dec
) to strip both keys without knowing either.
secret.py
import os
import re
from binascii import unhexlify, hexlify
from collections import Counter
pcap_path = "/home/havoc/Downloads/CTF/files/capture.pcap"
def extract_hex_from_bytes(data_bytes):
# Find sequences of hex characters (even length), likely separated by non-hex chars/newlines
text = ""
try:
text = data_bytes.decode("utf-8", errors="ignore")
except Exception:
text = "".join(chr(b) if 32 <= b < 127 else " " for b in data_bytes)
# Match long hex strings (at least 16 hex chars) to avoid noise
candidates = re.findall(r"\b[0-9a-fA-F]{16,}\b", text)
return candidates
# Try with scapy if available
packets_hex = []
try:
from scapy.all import rdpcap, Raw
pkts = rdpcap(pcap_path)
for p in pkts:
if Raw in p:
payload = bytes(p[Raw].load)
packets_hex.extend(extract_hex_from_bytes(payload))
except Exception:
# Fall back: scan the whole file as bytes and extract hex-looking strings
with open(pcap_path, "rb") as f:
data = f.read()
packets_hex = extract_hex_from_bytes(data)
# Deduplicate while preserving order
seen = set()
ordered_hex = []
for h in packets_hex:
if h.lower() not in seen:
seen.add(h.lower())
ordered_hex.append(h)
# Filter to even-length hex strings only
ordered_hex = [h for h in ordered_hex if len(h) % 2 == 0]
# Group by length and pick the most common length (likely the message length)
lengths = Counter(len(h) for h in ordered_hex)
common_len = None
if lengths:
common_len = lengths.most_common(1)[0][0]
ordered_hex = [h for h in ordered_hex if len(h) == common_len]
# -----------------------------------------------------------------
# XOR decryption part
# -----------------------------------------------------------------
from itertools import starmap
import operator
def hx(s):
return bytes.fromhex(s)
def bxor(a, b):
return bytes(x ^ y for x, y in zip(a, b))
if len(ordered_hex) >= 3:
enc_hex, enc2_hex, dec_hex = ordered_hex[:3]
enc = hx(enc_hex)
enc2 = hx(enc2_hex)
dec = hx(dec_hex)
# since enc2 = enc ^ server_key
server_key = bxor(enc, enc2)
# since secret = dec ^ server_key
secret = bxor(dec, server_key)
result = {
"enc": enc_hex,
"enc2": enc2_hex,
"dec": dec_hex,
"server_key_hex": server_key.hex(),
"secret_hex": secret.hex(),
"secret_utf8": None,
}
# Try decoding as UTF-8, otherwise fallback
try:
result["secret_utf8"] = secret.decode("utf-8")
except UnicodeDecodeError:
try:
result["secret_utf8"] = secret.decode("latin-1")
except Exception:
result["secret_utf8"] = None
print("\n[+] Extraction Results")
for k, v in result.items():
print(f"{k}: {v}")
else:
print("[-] Not enough hex strings extracted to perform XOR analysis.")
we got this as response
scriptCTF{x0r_1s_not_s3cur3!!!!}
RSA-1
lets dive in.first we analyse really to understand whats cooking behind the scenes of this RSA chall and confirming if Hastads Broadcast Attack Applies. heavy terminologies but dont worry (giigles) everything is cool.
lets dive in coz we already know the attack by name thats cool
Big up chat-gpt for the code though i refined it you assisted…..so after crafting a simple python script with my buddy that applies the Chinese Remainder Theorem which combines cipher texts via CRT and then extracts the e-th root to recover the message.Lets dive in:
chinese.py
import gmpy2
from Crypto.Util.number import long_to_bytes
n1 = 156503881374173899106040027210320626006530930815116631795516553916547375688556673985142242828597628615920973708595994675661662789752600109906259326160805121029243681236938272723595463141696217880136400102526509149966767717309801293569923237158596968679754520209177602882862180528522927242280121868961697240587
c1 = 77845730447898247683281609913423107803974192483879771538601656664815266655476695261695401337124553851404038028413156487834500306455909128563474382527072827288203275942719998719612346322196694263967769165807133288612193509523277795556658877046100866328789163922952483990512216199556692553605487824176112568965
n2 = 81176790394812943895417667822424503891538103661290067749746811244149927293880771403600643202454602366489650358459283710738177024118857784526124643798095463427793912529729517724613501628957072457149015941596656959113353794192041220905793823162933257702459236541137457227898063370534472564804125139395000655909
c2 = 40787486105407063933087059717827107329565540104154871338902977389136976706405321232356479461501507502072366720712449240185342528262578445532244098369654742284814175079411915848114327880144883620517336793165329893295685773515696260299308407612535992098605156822281687718904414533480149775329948085800726089284
n3 = 140612513823906625290578950857303904693579488575072876654320011261621692347864140784716666929156719735696270348892475443744858844360080415632704363751274666498790051438616664967359811895773995052063222050631573888071188619609300034534118393135291537302821893141204544943440866238800133993600817014789308510399
c3 = 100744134973371882529524399965586539315832009564780881084353677824875367744381226140488591354751113977457961062275480984708865578896869353244823264759044617432862876208706282555040444253921290103354489356742706959370396360754029015494871561563778937571686573716714202098622688982817598258563381656498389039630
e = 3
# List of (ciphertext, modulus) pairs
cts_mods = [(c1, n1), (c2, n2), (c3, n3)]
def chinese_remainder_theorem(congruences):
# congruences is a list of (remainder, modulus) pairs
N = 1
for _, n_i in congruences:
N *= n_i
x = 0
for r_i, n_i in congruences:
M_i = N // n_i
# Calculate M_i_inv = M_i^(-1) mod n_i
# Use gmpy2.invert for modular inverse
M_i_inv = gmpy2.invert(M_i, n_i)
x = (x + r_i * M_i * M_i_inv) % N
return x
# Apply CRT to the ciphertexts
# We need c_i = m^e mod n_i, so we use c_i as remainders
congruences = []
for c, n in cts_mods:
congruences.append((c, n))
combined_c = chinese_remainder_theorem(congruences)
# Now we have C = M^e mod (N1*N2*N3)
# Since M^e < N1*N2*N3, we can directly take the e-th root
m_e_root, is_perfect = gmpy2.iroot(combined_c, e)
if is_perfect:
print(f"Decrypted message (integer): {m_e_root}")
# Convert the integer message to bytes
decrypted_bytes = long_to_bytes(int(m_e_root))
print(f"Decrypted message (bytes): {decrypted_bytes}")
print(f"Flag: {decrypted_bytes.decode('utf-8')}")
else:
print("Could not find a perfect e-th root. Hastad's attack might not be applicable or failed.")
the response was as we expected cool and juicy!!!!
ubuntu@havoc:~ $ cd /home/ubuntu && python3 solve.py Decrypted message (integer): 6043384257179851698402764196375123148896204465004932561416076042681453019986274768617287472758478984797562240169432633876557803963678627742607532956455442 Decrypted message (bytes): b"scriptCTF{y0u_f0und_mr_yu's_s3cr3t_m3g_12a4e4}\x12\x12\x12\x12\x12\x12\x12\x12\x12\x12\x12\x12\x12\x12\x12\x12\x12\x12" Flag: scriptCTF{y0u_f0und_mr_yu's_s3cr3t_m3g_12a4e4} ubuntu@havoc:~ $
therefore
scriptCTF{y0u_f0und_mr_yu's_s3cr3t_m3g_12a4e4}
The next amazing challenge which i needed to share is:
Diskchal
Challenge Description
Tools Used
file
: To identify the file type of the disk image.parted
: To list partitions in the disk image.losetup
: To create a loop device from the disk image.mkdir
: To create a mount point.sudo mount
: To mount the disk image.fsck.vfat
: To check the integrity of the FAT32 filesystem.testdisk
: Data recovery utility (attempted, but not used for final solution).photorec
: File data recovery software (attempted, but not used for final solution).foremost
: Carves files based on headers and footers (attempted, but not used for final solution).scalpel
: Fast file carver (attempted, but not used for final solution).strings
: To extract printable strings from the disk image.binutils
: Provides thestrings
command.sleuthkit
(fls
,icat
): Digital forensics tools for filesystem analysis and file carving.gunzip
: To decompress the recovered file.
Solution Steps
1. Initial Analysis of the Disk Image
First, I checked the file type of the provided stick.img
to understand its basic structure.
file /home/ubuntu/upload/stick.img
Output:
/home/ubuntu/upload/stick.img: DOS/MBR boot sector, code offset 0x58+2, OEM-ID "mkfs.fat", sectors 49152 (volumes <=32 MB), Media descriptor 0xf8, sectors/track 32, heads 4, FAT (32 bit), sectors/FAT 378, serial number 0x8caae860, unlabeled
This indicated it’s a FAT32 filesystem.
Next, I tried to list partitions using fdisk
, but it was not installed. So, I installed parted
and used it to inspect the partition table.
sudo apt-get update && sudo apt-get install -y parted
parted /home/ubuntu/upload/stick.img print
Output:
Model: (file)
Disk /home/ubuntu/upload/stick.img: 25.2MB
Sector size (logical/physical): 512B/512B
Partition Table: loop
Disk Flags:
Number Start End Size File system Flags
1 0.00B 25.2MB 25.2MB fat32
This confirmed it’s a FAT32 filesystem and showed a single partition.
2. Attempting to Mount and Explore
I attempted to mount the disk image to explore its contents. First, I created a loop device and a mount point.
sudo losetup -f /home/ubuntu/upload/stick.img
losetup -a
sudo mkdir /mnt/stick
Then, I tried to mount it, but encountered issues with the filesystem type.
sudo mount -t vfat /dev/loop0 /mnt/stick
After installing dosfstools
and checking fsck.vfat
, the mounting still failed. This suggested that the filesystem might be corrupted or the files were deleted, making direct mounting difficult for recovery.
3. File Recovery Attempts (Foremost, Scalpel, PhotoRec)
Since direct mounting was problematic, I moved to file carving tools to recover potentially deleted files.but in vain ,,,like ilikuwa inanikula kichwa ….
4. Using SleuthKit for Deleted File Recovery
Given the previous attempts, I decided to use sleuthkit
, a suite of command-line tools for forensic analysis.
First, I installed sleuthkit
:
sudo apt-get install -y sleuthkit
Then, I used fls
to list files, including deleted ones, specifying the FAT32 filesystem and an offset of 0 (for the start of the image).
fls -o 0 -r -f fat32 /home/ubuntu/upload/stick.img
Output:
r/r 4: notes.txt
r/r 7: random_thoughts.txt
r/r * 10: secret_magic_collection.gz
v/v 773827: $MBR
v/v 773828: $FAT1
v/v 773829: $FAT2
V/V 773830: $OrphanFiles
I noticed secret_magic_collection.gz
with an asterisk (*
), which usually indicates a deleted file. The inode number for this file is 10
.
I used icat
to recover the content of this deleted file, redirecting the output to a new .gz
file.
icat -o 0 -f fat32 /home/ubuntu/upload/stick.img 10 > /home/ubuntu/secret_magic_collection.gz
Finally, I decompressed the .gz
file and viewed its content.
gunzip /home/ubuntu/secret_magic_collection.gz
cat /home/ubuntu/secret_magic_collection
Output:
scriptCTF{1_l0v3_m461c_7r1ck5}
Flag
The flag is: scriptCTF{1_l0v3_m461c_7r1ck5}
Next
Sums
Challenge Description
This a programming task that requires calculating the sum of numbers within specified ranges. The problem provides a server.py
script and instance information to connect to a remote server. The goal is to interact with this server, provide correct sums, and retrieve the flag.
Problem Analysis (server.py)
The server.py
script reveals the core logic of the challenge:
- Initialization: It generates
n = 123456
random integers between 0 and 696969. These numbers are then sent to the client. - Range Generation: It generates
n
random ranges[l, r]
, wherel
andr
are 0-indexed and inclusive. These ranges are also sent to the client. - Client Interaction: The server expects a program (referred to as
./solve
in the script) to take the generated numbers and ranges as input, calculate the sum for each range, and return these sums. - Verification: The server compares the sums received from the client with its own calculated sums. If they match and the entire process (from connection to receiving sums) completes within 10 seconds, the flag is read from
flag.txt
and printed.
The above setup indicates that the client needs to:
- Connect to the remote server.
- Receive a large list of numbers.
- Receive a large list of
[l, r]
ranges. - Efficiently calculate the sum for each
[l, r]
range. - Send these calculated sums back to the server.
- Capture the final output from the server, which should be the flag.
Solution Approach: Prefix Sums
Given the large number of elements (n = 123456
) and an equal number of range queries, a naive approach of summing elements for each range would be too slow (O(N*Q) where Q is the number of queries). A more efficient method is required. The prefix sum technique is ideal for this scenario.
How Prefix Sums Work:
- Pre-computation: Create a
prefix_sum
array whereprefix_sum[i]
stores the sum of all elements from the beginning of the original array up to indexi-1
. That is,prefix_sum[0] = 0
, andprefix_sum[i] = nums[0] + nums[1] + ... + nums[i-1]
. - Range Sum Query: The sum of elements within a range
[l, r]
(inclusive, 0-indexed) can be calculated in O(1) time using the formula:sum = prefix_sum[r+1] - prefix_sum[l]
.
This approach reduces the time complexity for calculating all range sums to O(N) for pre-computation and O(Q) for queries, resulting in an overall O(N+Q) complexity, which is efficient enough for the given constraints.
Implementation Details
Two Python scripts were developed:
solve.py
(local solution for testing - though not directly used against the remote server, it helped in understanding the logic): This script reads numbers and ranges from standard input, computes prefix sums, calculates range sums, and prints them to standard output. This was initially considered for local execution as./solve
but the remote server expects direct network interaction.import sys def solve(): nums_str = sys.stdin.readline().strip().split() nums = [int(x) for x in nums_str] n = len(nums) prefix_sum = [0] * (n + 1) for i in range(n): prefix_sum[i+1] = prefix_sum[i] + nums[i] results = [] for _ in range(n): line = sys.stdin.readline().strip().split() l, r = int(line[0]), int(line[1]) # Calculate sum for the range [l, r] (inclusive) results.append(str(prefix_sum[r+1] - prefix_sum[l])) sys.stdout.write(\'\\n\'.join(results) + \'\\n\') if __name__ == \'__main__\': solve()
ctf_client.py
(the main script to interact with the remote server): This script handles the network communication and implements the prefix sum logic.- Connection: Establishes a TCP connection to
play.scriptsorcerers.xyz
on port10382
. - Data Reception: It reads all incoming data from the server. The server first sends a line of space-separated numbers, followed by
n
lines, each containing two space-separated integers representingl
andr
for a range query. A timeout is used to determine the end of the initial data transmission. - Parsing: The received data is parsed to extract the initial list of numbers and all the range queries.
- Prefix Sum Calculation: The
prefix_sum
array is built from the received numbers. - Sum Calculation: For each
[l, r]
range, the sum is calculated using the prefix sum formula. - Sending Results: All calculated sums are joined by newlines and sent back to the server.
- Flag Retrieval: After sending the sums, the script waits to receive the final response from the server, which is expected to be the flag.
import socket import sys def solve_ctf(): # Connect to the server host = \'play.scriptsorcerers.xyz\' port = 10382 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) # Receive all initial data (numbers and ranges) received_data = b\'' sock.settimeout(1.0) # Set a timeout for receiving data while True: try: chunk = sock.recv(4096) if not chunk: break # Server closed the connection received_data += chunk except socket.timeout: break # No more data for a while, assume end of initial input except Exception as e: print(f"Error receiving data: {e}") sock.close() return data_str = received_data.decode() lines = data_str.strip().split(\'\\n\') # The first line contains the numbers, subsequent lines contain ranges nums_str = lines[0] nums = [int(x) for x in nums_str.split()] n = len(nums) # Build prefix sum array for efficient range sum queries prefix_sum = [0] * (n + 1) for i in range(n): prefix_sum[i+1] = prefix_sum[i] + nums[i] # Process range queries results = [] # Iterate through lines starting from the second line (index 1) for ranges for i in range(1, len(lines)): parts = lines[i].split() if len(parts) == 2: l, r = int(parts[0]), int(parts[1]) # Calculate sum for the range [l, r] (inclusive) range_sum = prefix_sum[r+1] - prefix_sum[l] results.append(str(range_sum)) # Send results back to server response = \'\\n\'.join(results) + \'\\n\' sock.sendall(response.encode()) # Receive the flag (or error message) flag_data = sock.recv(1024).decode() print(f"Flag: {flag_data.strip()}") sock.close() if __name__ == \'__main__\': solve_ctf()
- Connection: Establishes a TCP connection to
Flag
After running the ctf_client.py
script, the following flag was successfully retrieved:
scriptCTF{1_w4n7_m0r3_5um5_875c4c889b8f}
Next one is under programming was:
More Divisors
lets dive in:
The output contains a large list of numbers. I analyzed it offline, then wrote a script to find the longest subsequence with GCD > 1 by counting divisible numbers per prime factor. Next, I will run this script to determine the answer for the challenge.
`python script
import math
def gcd(a, b):
while b:
a, b = b, a % b
return a
def prime_factors(n):
factors = set()
d = 2
temp = n
while d * d <= temp:
if temp % d == 0:
factors.add(d)
while temp % d == 0:
temp //= d
d += 1
if temp > 1:
factors.add(temp)
return factors
with open('ctf_output.txt', 'r') as f:
numbers_str = f.read().strip()
numbers = [int(x) for x in numbers_str.split()]
# Dictionary to store counts of numbers divisible by each prime factor
prime_counts = {}
for num in numbers:
factors = prime_factors(num)
for factor in factors:
prime_counts[factor] = prime_counts.get(factor, 0) + 1
# The longest subsequence will be the maximum count for any prime factor
max_length = 0
if prime_counts:
max_length = max(prime_counts.values())
print(max_length)
Having retrieved the data, I analyzed the large number list offline and developed a script to find the longest subsequence with GCD > 1 by counting divisible numbers per prime factor. The script identified the maximum count as 73,062, which I submitted as the answer to the challenge. This confirms the approach to find the longest subsequence based on shared prime factors. The next step is to verify the server’s response for success.
ย submitted the calculated subsequence length (73,062) to the CTF server multiple times, capturing the response each time.
I submitted the correct subsequence length to the server and retrieved the flag. To ensure accuracy, I generated a script that reconnects, processes the data, and extracts the flag.
`solve and get flag.py
import socket
import math
import re
def gcd(a, b):
while b:
a, b = b, a % b
return a
def prime_factors(n):
factors = set()
d = 2
temp = n
while d * d <= temp:
if temp % d == 0:
factors.add(d)
while temp % d == 0:
temp //= d
d += 1
if temp > 1:
factors.add(temp)
return factors
HOST = 'play.scriptsorcerers.xyz'
PORT = 10100
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
data = b''
while True:
try:
chunk = s.recv(4096)
if not chunk:
break
data += chunk
if b'\n' in chunk: # Assuming numbers are sent line by line or with a final newline
break
except socket.timeout:
break
numbers_str = data.decode('utf-8')
numbers = [int(x) for x in numbers_str.split() if x.isdigit()]
prime_counts = {}
for num in numbers:
factors = prime_factors(num)
for factor in factors:
prime_counts[factor] = prime_counts.get(factor, 0) + 1
max_length = 0
if prime_counts:
max_length = max(prime_counts.values())
s.sendall(str(max_length).encode('utf-8') + b'\n')
flag_data = s.recv(4096)
print(flag_data.decode('utf-8'))
the response was cool and juicy!!!!!
The flag is:
scriptCTF{7H3_m0r3_f4C70r5_7h3_b3773r_252035e2f788}
THE NEXT COOL CHALLENGES WERE UNDER MISC:
Div
lets dive in to this div challenge under misc section.
I analyzed the chall.py
code. The objective is to make secret / fl_num
equal to 0. Since secret
is a large random integer, fl_num
must be a very large number to achieve this, considering the decimal.getcontext().prec = 50
setting.
analysis.py
import decimal
import os
# Set precision to 50 like in the challenge
decimal.getcontext().prec = 50
# Simulate the secret generation
secret = int(os.urandom(16).hex(), 16)
print(f"Example secret: {secret}")
print(f"Secret bit length: {secret.bit_length()}")
# The challenge requires secret / fl_num == 0
# This means fl_num must be infinity or secret must be 0
# Since secret is random and non-zero, we need fl_num to be infinity
# Test with very large numbers
test_numbers = [
"1e308", # This would be rejected due to 'e'
"9" * 9, # Maximum length without 'e'
"999999999", # 9 digits
]
for num_str in test_numbers:
if 'e' in num_str.lower():
print(f"{num_str}: Contains 'e', would be rejected")
continue
if len(num_str) >= 10:
print(f"{num_str}: Too long ({len(num_str)} chars), would be rejected")
continue
try:
fl_num = decimal.Decimal(num_str)
div = secret / fl_num
print(f"{num_str}: div = {div}")
print(f"div == 0: {div == 0}")
except Exception as e:
print(f"{num_str}: Error - {e}")
print("\n" + "="*50)
print("Analysis:")
print("- secret is a large random integer (up to 128 bits)")
print("- We need secret / fl_num == 0")
print("- fl_num must be decimal.Decimal(input)")
print("- Input cannot contain 'e' (no scientific notation)")
print("- Input must be < 10 characters")
print("- For division to equal 0, fl_num must be infinity")
# Test infinity
print("\nTesting infinity approaches:")
try:
inf_decimal = decimal.Decimal('inf')
div_inf = secret / inf_decimal
print(f"secret / Decimal('inf') = {div_inf}")
print(f"Is it 0? {div_inf == 0}")
except Exception as e:
print(f"Error with 'inf': {e}")
# Test very small numbers (close to 0)
print("\nTesting very small numbers:")
small_numbers = ["0.0000001", "1e-100"]
for num_str in small_numbers:
if 'e' in num_str.lower():
print(f"{num_str}: Contains 'e', would be rejected")
continue
if len(num_str) >= 10:
print(f"{num_str}: Too long, would be rejected")
continue
try:
fl_num = decimal.Decimal(num_str)
div = secret / fl_num
print(f"{num_str}: div = {div}")
except Exception as e:
print(f"{num_str}: Error - {e}")
response was
`ubuntu@havoc:~ $ cd /home/ubuntu && python3 analysis.py Example secret: 311763008669501265474399445670348425413 Secret bit length: 128 1e308: Contains 'e', would be rejected 999999999: div = 311763008981264274455663720126.01214553901214553901 div == 0: False 999999999: div = 311763008981264274455663720126.01214553901214553901 div == 0: False ================================================== Analysis: - secret is a large random integer (up to 128 bits) - We need secret / fl_num == 0 - fl_num must be decimal.Decimal(input) - Input cannot contain 'e' (no scientific notation) - Input must be < 10 characters - For division to equal 0, fl_num must be infinity Testing infinity approaches: secret / Decimal('inf') = 0E-1000048 Is it 0? True Testing very small numbers: 0.0000001: div = 3.11763008669501265474399445670348425413E+45 1e-100: Contains 'e', would be rejected
ubuntu@havoc:~ $`
The analysis confirms that secret / Decimal('inf')
results in 0E-1000048
, which evaluates to 0
. However, the input num
cannot contain ’e’ and must be less than 10 characters. This means we cannot directly input ‘inf’inf’ or ‘0.0’ (which would also result in a ZeroDivisionError). The problem states that fl_num
must be a decimal.Decimal
object. The decimal
module handles precision. If fl_num
is a very small number, close to zero, then secret / fl_num
would be a very large number. If fl_num
is a very large number, then secret / fl_num
would be a very small number. The key is that decimal.getcontext().prec = 50
. This means the precision is limited to 50 significant digits. If the result of the division is smaller than what can be represented with 50 significant digits, it will be rounded to zero. I need to find a number num
(less than 10 characters, no ’e’) such that secret / decimal.Decimal(num)
becomes effectively zero due to precision limits.
precision.py
import decimal
import os
# Set precision to 50 like in the challenge
decimal.getcontext().prec = 50
# Generate a large secret like in the challenge
secret = int(os.urandom(16).hex(), 16)
print(f"Secret: {secret}")
print(f"Secret length: {len(str(secret))}")
# Test with the string "inf" directly
test_inputs = ["inf", "Inf", "INF", "infinity"]
for inp in test_inputs:
if len(inp) >= 10:
print(f"'{inp}': Too long ({len(inp)} chars)")
continue
if 'e' in inp.lower():
print(f"'{inp}': Contains 'e'")
continue
try:
fl_num = decimal.Decimal(inp)
print(f"'{inp}': Decimal = {fl_num}")
div = secret / fl_num
print(f"'{inp}': Division result = {div}")
print(f"'{inp}': Is zero? {div == 0}")
print()
except Exception as e:
print(f"'{inp}': Error - {e}")
print()
# Test edge cases with very large numbers within constraints
print("Testing large numbers within constraints:")
large_nums = ["999999999", "9999999", "99999999"] # All under 10 chars
for num_str in large_nums:
try:
fl_num = decimal.Decimal(num_str)
div = secret / fl_num
print(f"'{num_str}': {div}")
print(f"'{num_str}': Is zero? {div == 0}")
print(f"'{num_str}': Type of result: {type(div)}")
print()
except Exception as e:
print(f"'{num_str}': Error - {e}")
print()
# Check what happens with precision limits
print("Testing precision behavior:")
decimal.getcontext().prec = 5 # Temporarily reduce precision
small_secret = 12345
large_num = decimal.Decimal("999999999")
result = small_secret / large_num
print(f"With prec=5: {small_secret} / {large_num} = {result}")
print(f"Is zero? {result == 0}")
# Reset precision
decimal.getcontext().prec = 50
response
`ubuntu@havoc:~ $ cd /home/ubuntu && python3 precision_test.py Secret: 98937599726391266187988492101342268578 Secret length: 38 'inf': Decimal = Infinity 'inf': Division result = 0E-1000048 'inf': Is zero? True 'Inf': Decimal = Infinity 'Inf': Division result = 0E-1000048 'Inf': Is zero? True 'INF': Decimal = Infinity 'INF': Division result = 0E-1000048 'INF': Is zero? True 'infinity': Decimal = Infinity 'infinity': Division result = 0E-1000048 'infinity': Is zero? True Testing large numbers within constraints: '999999999': 98937599825328866013317358114.659626692659626692660 '999999999': Is zero? False '999999999': Type of result: <class 'decimal.Decimal'> '9999999': 9893760962015222820321131242247.3510825351082535108 '9999999': Is zero? False '9999999': Type of result: <class 'decimal.Decimal'> '99999999': 989376007157672733456612255579.54524157545241575452 '99999999': Is zero? False '99999999': Type of result: <class 'decimal.Decimal'> Testing precision behavior: With prec=5: 12345 / 999999999 = 0.000012345 Is zero? False ubuntu@havoc:~ $`
The tests confirm that inputting ‘inf’ (or ‘Inf’, ‘INF’, ‘infinity’) works as decimal.Decimal('inf')
and results in secret / fl_num == 0
. The string ‘inf’ is 3 characters long and does not contain ’e’, satisfying all conditions. I will now connect to the CTF server and submit ‘inf’ as the number.
I confirmed that using ‘inf’ (or variants) as input results in secret / Decimal('inf')
being zero within 50-digit precision. ‘inf’ is 3 characters, contains no ’e’, and is accepted by the decimal
module. Therefore, I connected to the CTF server again and submitted ‘inf’ as the input to make the division zero and retrieve the flag.
Connecting to the CTF server and submiting the solution gave this cool flag
The flag is:
scriptCTF{70_1nf1n17y_4nd_b3y0nd_a476fcfb1cb9}
Next on misc
Emoji
๐ Challenge Description
Weโre given a file out.txt
filled with weird emoji-like characters that resemble Mahjong tiles / domino tiles rather than regular emojis. Example snippet:
๐ณ๐ฃ๐ฒ๐ฉ๐ฐ๐ด๐๐๐๐ป๐ณ๐ญ๐ฐ๐ช๐ฑ๐๐ณ๐ฎ๐ฃ๐ฐ๐ค๐ฑ๐ฎ๐ง๐๐ฑ๐ณ๐๐ท๐ณ๐ฑ๐ฒ๐ค๐๐ด๐ฎ๐ค๐๐ฆ๐ต๐ฎ๐ก๐ฑ๐ฅ๐ด๐ถ๐ค๐ฝ
Weโre also told the flag format is scriptCTF{...}
.
๐ Step 1: Identify the characters
Looking at the file, the characters are not standard smiley emojis. Instead, they come from Unicode block “Mahjong Tiles / Domino Tiles” (U+1F000
โ U+1F02F
).
So each “emoji” is really just a Unicode codepoint.
๐ Step 2: Extract numeric values
For each symbol:
- Take its Unicode codepoint.
Example:๐ณ โ U+1F073
- Subtract
0x1F000
from it.0x1F073 - 0x1F000 = 0x73
- Convert the result to ASCII.
0x73 = 's'
Repeating this for the rest yields meaningful text.
๐จ Step 3: Decode the entire file
Applying the above logic on all characters in out.txt
gives:
scriptCTF{3m0j1_3nc0d1ng_1s_w31rd_4nd_fun!1e46d}
โ Final Flag
scriptCTF{3m0j1_3nc0d1ng_1s_w31rd_4nd_fun!1e46d}
๐ง Takeaway
This was a fun stego/encoding challenge. The trick was recognizing that the โemojisโ werenโt random but came from the Mahjong tile Unicode block. Once mapped back to ASCII, the flag revealed itself.
The next challenge on the same module was:
SUBTRACT
Nice challenge! This oneโs a โscatter plotโ stego. Theย coordinates.txt
ย is a huge list of pixel coordinates within a 500ร500 canvas. If you plotย allย points, the image turns completely black (there are ~250k unique pixels), butย 573 pixels are duplicatedโand those duplicates are the actual signal.
I parsed the file, analyzed frequencies, and rendered several helpful images so i canย see the flag clearly:
I also tried the hint โSubtract 366โ as coordinate shifts (mod 500) in case the author wanted a wraparound translation:
And there we had our cool challenge flag:
scriptCTF{5ub7r4c7_7h3_n01s3}
And then finally the moment most were waiting the moment of truth…TRUE DETECTIVE…OSINT CHALLENGES
INSIDER
On this osint chall insider i managed to solve three out of 4 so lets dig in
so i managed to look on to the support team of the ctf and i managed to look them on discord and lemme tell you we found our nigga just relaxing on the profile picture of this dude
we got the flag as cool as that
INSIDER 2
The next was this:
so lets go my detectives:
so i had to google dork and find the github profile in question and all was not in vain i got it and we started our magic.On the said repository i found a cred.txt
meaning this were credentials to a certain platform so i had to become creative and find this platform and i fot it.
it was password and username
and boom we were in……..and the flag was there waiting for us to come…..
INSIDER 3
This was a bit easy didnt need alot of work so on the same github repository i saw onother commit on osint and said why not lemme see it and there it was ,just saying to me why are you late (laughs)
i managed those on osint.
Those were the challenges i saw was cool to share coz they had something juicy about them and may help in future researchs if there will be one (giggles)
Stay Curious and Keep Hacking!