Hello Navi

Tech, Security & Personal Notes

An investigator recovered a Linux disk image from a manuscript ward workstation in Varanasi. The drive appears normal, but operators suspect hidden transfer records were concealed using layered steganography and encryption workflows.

Initial Exploration

The provided disk image (kashi_ritual_ledger.img) is an ext4 filesystem.

1
2
3
4
5
6
7
8
9
❯ file kashi_ritual_ledger.img
kashi_ritual_ledger.img: Linux rev 1.0 ext4 filesystem data, UUID=f83285e2-bb1a-4a34-bdec-b901cf985c4e, volume name "KASHI_LEDGER" (extents) (64bit) (large files) (huge files)

# Mount the image
sudo mkdir -p /mnt/kashi_ledger
sudo mount -o loop,ro kashi_ritual_ledger.img /mnt/kashi_ledger

# Search for hidden files
find /mnt/kashi_ledger -type f -name ".*"

Exploring the filesystem reveals several key files: - challenge_runtime.json: Metadata containing passphrases and AES parameters. - /home/pandit_ved/: User home directory with Pictures/ward_scans, Notes, and a hidden .archive_payloads folder. - /deleted_mail_pool/: Contains .eml files discussing “passphrase doctrine.”

Metadata Analysis

Reading challenge_runtime.json provided the following stages: - Stage 1 Steg Passphrase: trishul-lantern-braid - Stage 2 Steg Phrase: ghat-manjari-copper-owl

Notes in home/pandit_ved/Notes/ritual_index_notes.md hinted that the “hidden ledger capsule is in one scan that does not open with the standard (Stage 1) phrase.”

Steganography Extraction

There were four BMP scans in the ward_scans directory. Using steghide with the Stage 1 passphrase:

1
2
3
for f in /mnt/kashi_ledger/home/pandit_ved/Pictures/ward_scans/*.bmp; do
steghide extract -sf "$f" -p "trishul-lantern-braid"
done

While most contained decoy files, scan_midnight_index.bmp failed, indicating it required the Stage 2 passphrase:

1
2
steghide extract -sf scan_midnight_index.bmp -p "ghat-manjari-copper-owl"
# Output: wrote extracted data to "stage2_ledger.enc"

Locating the Flag

The extracted stage2_ledger.enc matched a file in the hidden .archive_payloads directory. A corresponding .txt file was also present:

1
cat /mnt/kashi_ledger/home/pandit_ved/.archive_payloads/stage2_ledger.txt

Flag

kashiCTF{ledger_ashes_remember_every_ritual}

We sent the same announcement to three servers for redundancy. Each server has its own RSA key. Intercept all three — maybe you can piece something together.

Initial Analysis

The challenge provides an output.txt containing an exponent e = 3, three moduli (n1, n2, n3), and three ciphertexts (c1, c2, c3). By examining the data, we notice:

  1. Low Exponent: e = 3 is very small.
  2. Identical Ciphertexts: c1 = c2 = c3. This means the raw message M was not padded differently for each server.
  3. Magnitude of c: The ciphertext c is significantly smaller than any of the moduli n.

In RSA, the encryption process is c = Me (mod  n). Usually, Me is much larger than n. However, if Me < n, then the modulo operation has no effect, and c = Me.

Solution

Since c < ni and e = 3, the message M can be recovered simply by calculating the integer cube root of c: $$M = \sqrt[3]{c}$$

Implementation

Using Python and the gmpy2 library, we can solve for M:

1
2
3
4
5
6
7
8
9
10
11
12
13
import gmpy2
from binascii import unhexlify

# Intercepted ciphertext
c = 475436441896018898725156479190091126537849994697426945980826369000641892902004477923335055269088235139492237640527487698088281484953901383579636883543216552932099156009006828723690550706326538736801225046068870773990108130474408522838234755277972911893744937243892927414355347438993698991261629557719442242861719577879055371620865465785392597257968132649494474946507819896785671106833645551504301840437212737125

# Calculate the cubic root
m, exact = gmpy2.iroot(c, 3)

if exact:
# Convert integer to hex, then to ASCII
flag = unhexlify(hex(m)[2:]).decode()
print(f"Flag: {flag}")

Note: This challenge is a simplified version of Håstad’s Broadcast Attack. While Håstad’s attack typically uses the Chinese Remainder Theorem (CRT) to solve for Me when Me > ni, the small size of the message relative to the key size here allowed for a direct cubic root calculation.

Flag

kashiCTF{h4st4d_s4ys_sm4ll_3xp0n3nts_k1ll_RSA_br04dc4sts}

Join the RITSEC CTF Discord server to get the most up-to-date information about the competition. The flag can be found in the topic of the #announcements channel, or in the CTF kickoff announcement.

Initial Analysis

The challenge points to the RITSEC CTF Discord server as a source for competition updates and a hidden flag.

Solution

By checking the topic of the #announcements channel, the flag is readily available.

Flag

RS{p1r4t3_d1sc0rd}

A secret message has been passed down through generations since the time of the great Mahabharata war. Legend says that every 64 years, the keepers of this secret would encode the message once more to protect it from those who might seek to misuse its power. The message has traveled through 3136 years of history, from the ancient battlefields of Kurukshetra in 3136 BCE to the dawn of the Common Era.

Initial Analysis

The challenge provides two main clues:

  1. Mathematical Clue: The message has existed for 3136 years and was re-encoded every 64 years. $$\frac{3136}{64} = 49$$ This suggests the message has been recursively encoded 49 times.
  2. File Inspection: The provided file secret_message.txt is large (~59MB) and starts with the characters Vm0wd2Qy..., which is a classic signature for multiple layers of Base64 encoding.

Extraction & Decoding

We can use a Python script to iteratively decode the file 49 times. Each layer of decoding reduces the file size until the final plaintext flag is revealed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import base64

def solve():
# Read the initial encoded data
with open('secret_message.txt', 'r') as f:
data = f.read().strip()

# Iteratively decode 49 times
print("[*] Starting iterative Base64 decoding...")
for i in range(49):
try:
data = base64.b64decode(data).decode('utf-8')
except Exception as e:
print(f"[-] Error at iteration {i+1}: {e}")
break

print(f"[+] Final Decoded Message: {data}")

if __name__ == "__main__":
solve()

Flag

kashiCTF{th3_s3cr3t_0f_mah4bh4r4t4_fr0m_3136_BCE}

I like to save my files as pdfs. Kashi kings hate 184.

Initial Analysis

The file is named flag.pdf, but standard PDF analysis tools fail to recognize it:

1
2
pdfid flag.pdf
# Output: Not a PDF document

Using the file command reveals its true identity:

1
2
file flag.pdf
# Output: flag.pdf: Netpbm image data, size = 284 x 150, rawbits, pixmap

The challenge hint “Kashi kings hate 184” suggests that the height of the image (currently 150) has been tampered with and should likely be 184.

Extraction & Decoding

Netpbm (PPM) files use a plain-text header. We can repair the height by editing the file header. Opening the file in a hex editor or a text editor like vim (using :set binary or simply editing the ASCII header) reveals:

1
2
3
4
P6
284 150
255
[Binary Data...]

Change the height value from 150 to 184:

1
2
3
4
P6
284 184
255
[Binary Data...]

After saving the change, the image can be opened with a standard image viewer to reveal the flag.

Flag

kashiCTF{iLOVEkashi}

Generating primes is expensive. I optimized my key generation to be twice as fast. The modulus is 4096 bits — perfectly secure.

Initial Analysis

The hint “optimized my key generation to be twice as fast” suggests that instead of generating two distinct large primes, the author might have reused the same prime (n = p2) or chosen two primes that are extremely close to each other. This makes the modulus vulnerable to Fermat’s Factorization Method or simply taking the square root.

Extraction & Decoding

Given a 4096-bit modulus n, we can check if it’s a perfect square or if its factors are close to $\sqrt{n}$ by starting from $\lfloor\sqrt{n}\rfloor$ and searching downwards. Once factored, we calculate ϕ(n), derive the private key d, and decrypt the AES key which was used to encrypt the flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import math
import base64
from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.Util.Padding import unpad
from Crypto.Cipher import AES

# Modulus and public exponent
n = 0x752a94a112ba0ce096f47934dac094d3d07b8c036613938142c0d4c15fc82692eee38d2457dd8d16472c4ddbe8cb5e2a6331e0ca0351094fc9516559768ebfe44509154d64116fa1fe1daf698413d37c9fe3406555f3190e29d99bee0cdd663d531c8e818f2686c7ad24338b4e93c6bfbd1b5a6dc5161316b2cb9ac1ae05a4ac43fdeb3b024b2e00dfcd87069ea1645996d9ad16ac3a9697414c17279112303a1d21136a99dc47628e15a3d6e18779de7aec310331dff1a81871b03e214de09f56c0f3de02f9f399be4ebc094f34b578d311a8b48e9c6cf2fa2f4e321f1dab0a99e5b9d99464c19452d9cc21544ac8e32fb9f13d1b2990758de0876de465cbd3632f846ef49fd7b97abee2ce529cfbc75a0d0792df6cc8091198134e9f646cf7d33c85c4ddd2c4b9a248c2c470d7369ebc7245bcec049455da2ceb742b26058514418398149d03cd1ad74a997375d0462a43e73aa62fc1f7e0dcc67e8f1559073074b9b8d3c37edfcfce67fd1822227933c5a14425d76119fc0a25da4c059761c86bc3077c4d096d9b9f6ae2faf728dbf24d48fa74d99c8c0d8780d2963ca9eccef0dd847ce22fc5b13793981257a9d4dd1af965e9baa5bd9fc4e3321cf8c6fd9871e342e5ae0dff19ab6e9fd8e14b5cc766b92df3306ef63af248b019528928644007c17e31918f9fdf10daeadc1eb8abeb6297bdc8f8e9b27c591f12159479
e = 65537

# Factor n
p = math.isqrt(n)
while n % p != 0:
p -= 1
q = n // p

# Calculate phi
if p == q:
phi = p * (p - 1)
else:
phi = (p - 1) * (q - 1)

# Decrypt AES Key
d = pow(e, -1, phi)
ct_b64 = "MVoAMG4KRlXdSxEtVL/GpwXWyelWmAsJJhMnPaTzF2Hhjm/h/vkHJKQIyHSld2XuB6Q0sWq/TN1dkSePEB1oq6ugzMcgp5VQ9Xn0mOCn1GZ7fP3bKUVD6mG1bt3dkHxzlAT7v6c6xKaTtJJKTV3JXnB2u3duW4dCrFyassEjHM1PEML3CWqJBrgxnlTIYI4i9ydwg1MF1fTOBTm2KLlQ6rIGnBWcGwg4k+8I7e0mIbhod53w3FzIPXePv0ONVp+HBn4XsICwEHhJLXixmHcEz0jxwCgRdz9qr+Ur1pOaVuKN/26cwpYdVTlY5Fk7KapoV3Ews69353gCa+QYiJtzuwr1uFTBe74GfZZ8xzxFK51TnMbqB1M5cpzBK8/TK+ES/+yy1R4jsGkQ4i7Qz0oAsqBoNExSaLsNgwzZe4dYRE2BwDy2tW2QAYFUeU+SVjUb2BI67QtQYl2g+GB0kAMzbcRFr/kykIHqqb2N05BxgRrtsFjq2zWvpJJ71OFCrWVg8IzO7N+WUXFFyFv7ZfdnKOhC/iPNNmLbJXa3Gul89Fa6VyvCJw6lA/t7QCsjtcVG7ox51JAvoHhw5FLIpT+wmfq969iUujkXzLpjyXBVrfnEdjRt61zGEd3tmWIYst721GcKnbxKBehxwpseDBYR0hJRy+CIf5SnrP6/Blq55cQ="
ct_int = bytes_to_long(base64.b64decode(ct_b64))
aes_key = long_to_bytes(pow(ct_int, d, n))

# Decrypt Flag
iv = base64.b64decode("XSCnpZLyN1Oin7F67hOKWQ==")
flag_ct = base64.b64decode("n+H1n3ezKEm0ulyLMcp/ShxLZAddKX7y848o/Lf/56qDev/DPBz+IRcJ14yHWGOuodMaMwyLZi9er7slNa+QMw==")

cipher = AES.new(aes_key, AES.MODE_CBC, iv)
flag = unpad(bytes(cipher.decrypt(flag_ct)), AES.block_size).decode()

print(f"\n[+] FLAG: {flag}")

Flag

kashiCTF{wh3n_0n3_pr1m3_1s_n0t_3n0ugh_p_squared_1s_w0rs3}

Wait, you forgot to give me the Advent calendar!?!?! And what is that supposed to mean, I should have opened the doors up until today?? Ugh, okay, then I’ll start: Door 1

Investigation

The hint suggests that we need to visit the “doors” of the Advent calendar. The URL structure was identified as:

1
GET https://hack.arrrg.de/adventskalender/$$ HTTP/1.1

Where $$ represents the door number. To find the flag, we need to check all possible doors (1 to 30).

Solution

Using Zaproxy (OWASP ZAP) or a similar fuzzer, we can automate the process:

  1. Intercept/Send Request: Create a base request to https://hack.arrrg.de/adventskalender/1.
  2. Set Payload: In Zaproxy’s fuzzer, set the payload for the door number position.
  3. Configure Generator: Use a Numberzz generator:
    • From: 1
    • To: 30
    • Step: 1
  4. Execute Fuzz: Start the fuzzer and monitor the responses.
  5. Analyze Results: Look for a response that differs in content.

After fuzzing, collecting the responses and looking for a secret message was revealed the hidden message.

THE ANSWER: FELIZNAVIDAD

I like to have my poems saved on CTFd instances like this. But the admin does like it, so I hid the secret in my poem.

Solution

The provided poem contains a significant amount of trailing tabs and spaces at the end of each line. This is a classic indication of SNOW (Steganographic Nature Of Whitespace) steganography.

By using a SNOW decoder (such as the web-based snow.js decoder), we can extract the hidden message from the poem text:

1
2
3
4
5
6
7
8
9
10
11
12
13
The way a crow	      	  	   	   		     	   	      
Shook down on me
The dust of snow
From a hemlock tree

Has given my heart
A change of mood
And saved some part
Of a day I had rued.




The decoded message reveals the flag.

Flag

kashiCTF{1_like_poems_but_1_lik3_u_more<3}

Its Time to be sane. I wish I could give the flag, but I can’t. Try searching this site. xd :>

Solution

The flag was split into two parts found in different locations on the platform:

  1. robots.txt: Navigating to https://kashictf.iitbhucybersec.in/robots.txt revealed the first part of the flag:

    1
    2
    3
    User-agent: *
    Disallow: /admin
    kashiCTF{50_you_did

  2. Home Page Source: Inspecting the HTML source code of the main landing page https://kashictf.iitbhucybersec.in/ revealed the second part hidden inside a comment:

    1
    <!--- _endup_ge77ing_the_flag_hehe} --->

Combining both parts yields the full flag.

Flag

kashiCTF{50_you_did_endup_ge77ing_the_flag_hehe}

The flag was hidden in the #rules channel of the kashiCTF Discord server.

Solution

The following encoded string was found:

1
Tk5RWEcyREpJTktFTTYzVU5CVVhHWDNYTUZaVjYzVFBPUlBXUVlMU01SNlFVPT09Cg==
  1. Base64 Decoding: Decoding the string once results in a Base32-encoded string.
    1
    NNQXG2DJINKEM63UNBUXGX3XMFZV63TPORPWQYLSMR6QU===
  2. Base32 Decoding: Decoding the resulting string

Flag

kashiCTF{this_was_not_hard}