Logo Havoc Hacking Articles

scriptCTF Writeup

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

Aug 18, 2025 - 23 minute read
feature image ctf

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


sec

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:
  1. `enc = secret โŠ• user_key
  2. enc2 = enc โŠ• server_key
  3. User sends back dec = enc2 โŠ• user_key
  4. 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

  1. Parse the .pcap to extract hex payloads.
  2. Identify which are enc, enc2, and dec.
  3. 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

flag

scriptCTF{x0r_1s_not_s3cur3!!!!}


RSA-1

rsa

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 the strings 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:

  1. Initialization: It generates n = 123456 random integers between 0 and 696969. These numbers are then sent to the client.
  2. Range Generation: It generates n random ranges [l, r], where l and r are 0-indexed and inclusive. These ranges are also sent to the client.
  3. 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.
  4. 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:

  1. Pre-computation: Create a prefix_sum array where prefix_sum[i] stores the sum of all elements from the beginning of the original array up to index i-1. That is, prefix_sum[0] = 0, and prefix_sum[i] = nums[0] + nums[1] + ... + nums[i-1].
  2. 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:

  1. 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()
    
  2. 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 port 10382.
    • 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 representing l and r 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()
    

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:

  1. Take its Unicode codepoint.
    Example: ๐Ÿณ โ†’ U+1F073
  2. Subtract 0x1F000 from it. 0x1F073 - 0x1F000 = 0x73
  3. 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!