CyberKavach QuestCon Series: Upside-Down Vault
🧩 CTF Write-Up: Upside-Down Vault
Author: Sai Veer
Flag Format: QUESTCON{...}
Challenge Type: Multi-Layer Crypto + Stego + Web Service
Difficulty: Medium–Hard
🗃️ Files Provided
encrypted_flag.bin
layer1_puzzle.json
layer2_blob.json
layer2.enc
privkey.enc
vault.py
vault_pub.pem
vault_secret.png
🧠 Challenge Overview
The challenge consists of:
-
3 cryptographic layers
-
1 steganographic HMAC extraction
-
1 server verification stage
The flag is revealed only after:
-
Extracting the hidden HMAC secret from
vault_secret.png. -
Solving all cryptographic layers.
-
Using the recovered secrets to correctly sign and “seal” a request to the local server (
vault.py). -
Finally, retrieving the decrypted flag from
/sealed.
🧩 Step-by-Step Walkthrough
Step 1 — Extract HMAC Secret from PNG
The image vault_secret.png hides an HMAC key in its LSBs.
Why
The server expects this secret in ctf_secret/secret_mac.
It’s later used to generate HMAC signatures for /sign requests.
Script — extract_hmac_from_vault_secret.py
#!/usr/bin/env python3
from PIL import Image
def extract(path='vault_secret.png'):
img = Image.open(path)
w, h = img.size
pix = img.load()
def gen_positions(n):
pos = []
x = 13
for i in range(n):
pos.append((x % w, (i * 7) % h))
x = (x * 1103515245 + 12345) & 0xFFFFFFFF
return pos
positions = gen_positions(4096)
bits = []
for x, y in positions:
r, g, b = pix[x, y]
bits.append(str(r & 1))
data = []
for i in range(0, len(bits), 8):
bb = bits[i:i+8]
if len(bb) < 8:
break
val = int(''.join(bb), 2)
if val == 0:
break
data.append(val)
mask = [0x5F, 0xA3, 0xC7, 0x1D]
out = bytes([data[i] ^ mask[i % len(mask)] for i in range(len(data))])
print(out.decode())
if __name__ == '__main__':
extract()
Run
python3 extract_hmac_from_vault_secret.py
Output
stego_hmac_1b047faecc67feb3
Save this:
HMAC_SECRET=$(python3 extract_hmac_from_vault_secret.py)
Used later in /sign HMAC and ctf_secret/secret_mac.
Step 2 — Factor Fermat-Friendly Composite
layer1_puzzle.json contains an RSA-style modulus N = p * q, where p and q are close.
Why
Factoring gives us p and q.
p (in hex) is used to derive the next AES key.
Script — fermat_solve.py
#!/usr/bin/env python3
from math import isqrt
import json, sys
N = int(json.load(open("layer1_puzzle.json"))["N"])
a = isqrt(N)
if a * a < N:
a += 1
while True:
b2 = a * a - N
b = isqrt(b2)
if b * b == b2:
p = a - b
q = a + b
print(p)
print(q)
sys.exit(0)
a += 1
Run
python3 fermat_solve.py > factors.txt
Output:
p = 1586187469...
q = 1586187469...
Convert p to hex for the next layer:
python3 - <<'PY' > pass1_hex.txt
p = int(open('factors.txt').read().splitlines()[0])
print(format(p, 'x'))
PY
Step 3 — Derive key2 & Decrypt layer2.enc
Why
layer2.enc is AES-CBC encrypted with:
key2 = SHA256(pass1_hex)
Generate key2
python3 - <<'PY'
import hashlib
p = open('pass1_hex.txt').read().strip()
open('key2.bin','wb').write(hashlib.sha256(p.encode()).digest())
print("sha256(pass1_hex) =", hashlib.sha256(p.encode()).hexdigest())
PY
Decrypt layer2
python3 decrypt_layer2.py
Output JSON
{
"vigenere_ct": "naKnlKOknqyWnmtla2meZm5uamuYnWtkmQ==",
"note": "Vigenere key is the decimal string of (p mod 10000)."
}
Step 4 — Vigenère Decryption
Why
The ciphertext above is base64-encoded and Vigenère-encrypted using the key:
key = str(p % 10000)
Script — vigenere_decrypt.py
#!/usr/bin/env python3
import json, base64
p = int(open('factors.txt').read().splitlines()[0])
doc = json.load(open('layer2_decrypted.json'))
ct = base64.b64decode(doc['vigenere_ct'])
vkey = str(p % 10000).encode()
pt = bytes((ct[i] - vkey[i % len(vkey)]) % 256 for i in range(len(ct)))
open('pass2.txt', 'wb').write(pt)
print("pass2 preview:", pt.decode())
Run
python3 vigenere_decrypt.py
Output
pass2 preview: finalkey_e2240e37518ad21b
Step 5 — Decrypt RSA Private Key
Why
privkey.enc is AES-CBC encrypted using:
key3 = SHA256(pass2)
Decrypt
python3 - <<'PY'
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
pass2 = open('pass2.txt','rb').read().strip()
key3 = hashlib.sha256(pass2).digest()
ct = open('privkey.enc','rb').read()
iv, body = ct[:16], ct[16:]
privpem = unpad(AES.new(key3, AES.MODE_CBC, iv).decrypt(body), 16)
open('vault_priv.pem','wb').write(privpem)
print("vault_priv.pem written, size:", len(privpem))
PY
✅ vault_priv.pem successfully generated.
Optionally verify it matches the public key:
python3 - <<'PY'
from Crypto.PublicKey import RSA
priv = RSA.import_key(open('vault_priv.pem','rb').read()).publickey().export_key()
pub = open('vault_pub.pem','rb').read()
print('pubs match?', priv.strip() == pub.strip())
PY
Step 6 — Start Server & Retrieve Flag
Prepare secrets
mkdir -p ctf_secret
echo -n "$HMAC_SECRET" > ctf_secret/secret_mac
chmod 600 ctf_secret/secret_mac
echo -n "lab_vault_t" > sealed
Run the server
python3 vault.py
# Running on http://127.0.0.1:5001
Request a signed token
export SECRET="$HMAC_SECRET"
payload="unlock_vault"
solutions="0|0|0"
mac=$(python3 -c "import hmac,hashlib,os; s=os.environ['SECRET'].encode(); msg=(\"$payload\"+'|'+'$solutions').encode(); print(hmac.new(s,msg,hashlib.sha256).hexdigest())")
curl -s -X POST http://127.0.0.1:5001/sign \
-H "Content-Type: application/json" \
-d "{\"payload\":\"$payload\",\"solutions\":[0,0,0],\"mac\":\"$mac\"}" | jq .
✅ Successful output:
{
"signature": "BASE64_SIG_STRING..."
}
Retrieve the flag
curl -s http://127.0.0.1:5001/sealed | jq .
Output
{
"flag": "QUESTCON{Yo3_cant_spell_@merican_without_3rica}"
}
🎉 FLAG → QUESTCON{Yo3_cant_spell_@merican_without_3rica}
🧩 Vulnerability Summary
| Layer | Weakness | Explanation |
|---|---|---|
| 1 | Fermat-friendly RSA | p and q are close, easy to factor. |
| 2 | Predictable Vigenère key | key = str(p % 10000) makes brute-force trivial. |
| 3 | Layered symmetric derivation | Chained SHA256/AES scheme builds on recoverable values. |
| 4 | Stego HMAC | LSB steganography with predictable positions & small XOR mask. |
| 5 | Server gating | Local files and correct HMAC simulate secure gate logic. |
🏁 Final Notes
-
Ensure no trailing newlines in
ctf_secret/secret_macorsealed. -
Always use raw byte operations for Vigenère decryption.
-
The challenge is an exercise in multi-layer key derivation, weak crypto design, and practical forensics.

Comments
Post a Comment