HSCTF 9 Writeup

この大会は2022/6/6 4:00(JST)~2022/6/11 4:00(JST)に開催されました。
今回もチームで参戦。結果は3424点で804チーム中74位でした。
自分で解けた問題をWriteupとして書いておきます。

discord-flag (miscellaneous)

Discordに入り、#announcementsチャネルのメッセージを見ると、フラグが書いてあった。

flag{wait, it's all math? always has been}

the-great-directory-egg-hunt (miscellaneous)

ディレクトリが深くまでたくさんあり各最下層にファイルがあるので、"flag"という文字列が含まれているファイルを検索する。

$ grep -r flag .
./dir/Tw/3l/v3/_D/1r/s_/D3/3p/_6/Gi/TQ/Ez/file.txt:If you followed the directories to here, you have found the flag!
./dir/Tw/3l/v3/_D/1r/s_/D3/3p/_6/Gi/TQ/Ez/file.txt:Use the directory names (without slashes) as part of the flag, then wrap it in the flag format "flag{}".

目的のファイルが見つかり、フラグはディレクトリ階層から"/"を除いたものを"flag{}"でラップしたものになる。

flag{Tw3lv3_D1rs_D33p_6GiTQEz}

onchain-baller (miscellaneous)

https://ropsten.etherscan.io/でこのアドレスを検索する。

0x339d58bff8b8C5dAAfaF08fA77ad39C7909B194E

イベント開始時点のトランザクションは以下の3つ。

0xca8190cf4c01a5ba4e60b6c9ae7e8b94a804b6b829f3e1404cc636dea5d6e792
0xa59cba5bfaabebcbe182b7b33bd614d9e05c5cd40da096a7e2b77bc30c62f129
0x4a30fd5405c44a2b7720f70bde5bc60100d8d968f94a0010c630259afa16bb85

順に詳細を見ていくことにする。最初のトランザクションハッシュのInput Dataにこう書いてある。

0x68736374667b315f623431315f306e5f346e645f3066665f7468335f636834316e7d

hexデコードする。

$ echo 68736374667b315f623431315f306e5f346e645f3066665f7468335f636834316e7d | xxd -r -p
hsctf{1_b411_0n_4nd_0ff_th3_ch41n}
hsctf{1_b411_0n_4nd_0ff_th3_ch41n}

lcvc (cryptography)

flag{}の中の文字列について、インデックスごとにstateが決まり、その分だけシフトしているので、元に戻す。

#!/usr/bin/env python3
alphabet = 'abcdefghijklmnopqrstuvwxyz'

ciphertext = 'mawhxyovhiiupukqnzdekudetmjmefkqjgmqndgtnrxqxludegwovdcdmjjhw'

state = 1
flag = ''
for character in ciphertext:
    state = (15 * state + 18) % 29
    flag += alphabet[(alphabet.index(character) - state) % 26]
flag = 'flag{%s}' % flag
print(flag)
flag{iguessthisiswhatyouwouldcallalinearcongruentialvigenerecipher}

travelling-salesman (algorithms)

提示される数字を最短でたどる順番を答えていく。小さい順にソートすれば、最短距離の一つの方法になるはず。

#!/usr/bin/env python3
import socket

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('travelling-salesman.hsctf.com', 1337))

data = recvuntil(s, b'\n').rstrip()
print(data)

for i in range(5):
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    chal = eval(data)

    ans = ' '.join([str(i) for i in sorted(chal)])
    data = recvuntil(s, b': ')
    print(data + ans)
    s.sendall(ans.encode() + b'\n')

data = recvuntil(s, b'\n').rstrip()
print(data)

実行結果は以下の通り。

== proof-of-work: disabled ==
[87, 10, 33, 85]
order: 10 33 85 87
[19, 55, 39, 83, 13, 40, 35, 15, 10, 72]
order: 10 13 15 19 35 39 40 55 72 83
[10, 85, 86, 76, 61, 50, 26, 92, 59, 13, 64, 18, 84, 25, 93, 95, 88, 70, 62, 98, 89, 35, 40, 44, 72, 51, 27, 34, 38, 33]
order: 10 13 18 25 26 27 33 34 35 38 40 44 50 51 59 61 62 64 70 72 76 84 85 86 88 89 92 93 95 98
[87, 80, 42, 53, 88, 75, 25, 92, 79, 51, 20, 35, 44, 97, 40, 61, 77, 18, 86, 45, 49, 76, 56, 78, 47, 94, 69, 83, 31, 32, 13, 11, 70, 46, 71, 93, 24, 34, 29, 99, 50, 95, 85, 58, 14, 33, 16, 19, 43, 90]
order: 11 13 14 16 18 19 20 24 25 29 31 32 33 34 35 40 42 43 44 45 46 47 49 50 51 53 56 58 61 69 70 71 75 76 77 78 79 80 83 85 86 87 88 90 92 93 94 95 97 99
[58, 61, 54, 95, 81, 23, 66, 39, 80, 99, 35, 20, 51, 57, 90, 78, 11, 19, 75, 77, 89, 71, 44, 32, 69, 25, 83, 55, 94, 65, 46, 41, 36, 70, 29, 72, 28, 24, 52, 49, 85, 79, 63, 37, 50, 93, 12, 87, 62, 43]
order: 11 12 19 20 23 24 25 28 29 32 35 36 37 39 41 43 44 46 49 50 51 52 54 55 57 58 61 62 63 65 66 69 70 71 72 75 77 78 79 80 81 83 85 87 89 90 93 94 95 99
flag{the_fitness_gram_pacer_test_is_a_multistage_aerobic_capacity_test_8182295882010254837}
flag{the_fitness_gram_pacer_test_is_a_multistage_aerobic_capacity_test_8182295882010254837}

gallery (web)

2 + 2 == 5には絶対ならないので、http://35.192.129.251:8003/flagでflagを表示させることはできない。
http://35.192.129.251:8003/image?image=<ファイル名>で、表示させることができそうだが、ファイル名に".jpg"が含まれている必要がある。
いろいろ試したところ、http://35.192.129.251:8003/image?image=back.jpg/../../flag.txtにアクセスすると、フラグが表示された。

flag{1616109079_is_a_cool_number}

quagmire-i (cryptography)

Quagmire Iの暗号とのこと。https://sites.google.com/site/cryptocrackprogram/user-guide/cipher-types/substitution/quagmireを参考に復号する。平文キーワードは以下のように縮小し、未使用のアルファベットを追加する。

CONLYRKSABDEFGHIJMPQTUVWXZ

上記の文字列で、Aの位置にHSCTFになるようアルファベットを並べる。

Keyed pt CONLYRKSABDEFGHIJMPQTUVWXZ
    H  1 ZABCDEFGHIJKLMNOPQRSTUVWXY
    S  2 KLMNOPQRSTUVWXYZABCDEFGHIJ
    C  3 UVWXYZABCDEFGHIJKLMNOPQRST
    T  4 LMNOPQRSTUVWXYZABCDEFGHIJK
    F  5 XYZABCDEFGHIJKLMNOPQRSTUVW

あとはこのテーブルを元に順番に復号する。

暗号文:LZXORNZBUYWNRARNOVGCLSQWJEFJFE

 1文字目:L --(1行目でLのもの)--> F
 2文字目:Z --(2行目でZのもの)--> I
 3文字目:X --(3行目でXのもの)--> L
 4文字目:O --(4行目でOのもの)--> L
 5文字目:R --(5行目でRのもの)--> T
 6文字目:N --(1行目でNのもの)--> H
 7文字目:Z --(2行目でZのもの)--> I
       :

これをスクリプトで実行する。

#!/usr/bin/env python3
ct = 'LZXORNZBUYWNRARNOVGCLSQWJEFJFE'

pt_tbl = 'CONLYRKSABDEFGHIJMPQTUVWXZ'
ct_tbls = [
    'ZABCDEFGHIJKLMNOPQRSTUVWXY',
    'KLMNOPQRSTUVWXYZABCDEFGHIJ',
    'UVWXYZABCDEFGHIJKLMNOPQRST',
    'LMNOPQRSTUVWXYZABCDEFGHIJK',
    'XYZABCDEFGHIJKLMNOPQRSTUVW'
]

flag = ''
for i in range(len(ct)):
    index = ct_tbls[i % 5].index(ct[i])
    flag += pt_tbl[index]

flag = 'flag{%s}' % flag
print(flag)
flag{FILLTHISBOWLWITHYOURFAVEFRUITS}

baby-baby-rsa (cryptography)

p, qの2進数をそれぞれ3分割してシャッフルしている値がわかっている。ブルートフォースでp, qから復号し、フラグの形式になるものを探す。

#!/usr/bin/env python3
from Crypto.Util.number import *
import itertools

def is_printable(s):
    for c in s:
        if c < 32 or c > 126:
            return False
    return True

e = 0x10001

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

c = int(params[0])
parts = eval(params[1])

for v in itertools.permutations(parts, 6):
    p = int(v[0] + v[1] + v[2], 2)
    q = int(v[3] + v[4] + v[5], 2)
    n = p * q
    phi = (p - 1) * (q - 1)
    d = inverse(e, phi)
    m = pow(c, d, n)
    flag = long_to_bytes(m)
    if is_printable(flag):
        print(flag.decode())
        break
flag{flbg{flcg{fldg{fleg}}}}

adding (reversing)

uptoの値を変えて試す。

upto=0
[20, 20, 10]
※20 * 2 + 10

upto=1
[20, 20, 10, 20, 20, 20, 20, 20, 20, 20]
※前の配列 + 20 * 6 + 20

upto=2
[20, 20, 10, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 30]
※前の配列 + 20 * 14 + 30

upto=3
[20, 20, 10, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 30, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 40]
※前の配列 + 20 * 30 + 40
 =前の配列 +20 * (2 + 4 + 8 + 16) + 10 * 4
 =20 * (2 + (2 + 4) + (2 + 4 + 8) + (2 + 4 + 8 + 16)) + 10 * (1 + 2 + 3 + 4)

以上から、以下の関数定義ができる。

sum(0) = 50
sum(n) = sum(n-1) + 20 * (2 * (2**(n + 1) - 1)) + 10 * (n + 1)

スクリプトにし、算出する。

#!/usr/bin/env python3
def calc(n):
    if n == 0:
        return 50
    return calc(n - 1) + 20 * (2 * (2**(n + 1) - 1)) + 10 * (n + 1)

upto = 213
flag = calc(upto)
flag = 'flag{%d}' % flag
print(flag)
flag{2106245833371143733958360553673408646377901908010982225086219772130}

atcs-nightmare (reversing)

コードを読むと、入力するフラグに以下の条件があることがわかる。

・長さは34
・先頭4バイトは"flag"、最後は"}"
・linkDemLists(recurses(stackAttack(f), "", 1))が"20_a1qti0]n/5f642kb\\2`qq4\\0q"と同じ
 ・stackAttack(f)
  ・最後の文字から順にASCIIコードで(i % 4)を引いた値の文字を結合
 ・resurses(in, "", 1)
  ※in: abcdefghの場合、hfdbacegの順に変わる。
 ・linkDemLists(in)
  ※in: abcdefghの場合、efghdcbaの順に変わる。

以上を元に"20_a1qti0]n/5f642kb\\2`qq4\\0q"から入力文字列に戻していく。

#!/usr/bin/env python3
def rev_stackAttack(s):
    d = ''
    for i in range(len(s)):
        d += chr(ord(s[i]) + i % 4)
    return d[::-1]

def rev_recurses(s):
    l = len(s) // 2
    s1 = s[l:]
    s2 = s[:l][::-1]
    return ''.join([s1[i] + s2[i]for i in range(l)])

def rev_linkDemLists(s):
    l = len(s) // 2
    return s[l:][::-1] + s[:l]

ct = "20_a1qti0]n/5f642kb\\2`qq4\\0q"

flag = rev_stackAttack(rev_recurses(rev_linkDemLists(ct)))
flag = 'flag{%s}' % flag
print(flag)
flag{th15_15nt_r0ck3t_sc1nc3_7272}

otp (cryptography)

平均すると、seedの期待値は100000000000の1/4。その周辺でブルートフォースでフラグの形式に復号できるものを探す。

#!/usr/bin/env python3
import random
from Crypto.Util.number import *

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

l = int(params[0])
flag_int = int(params[1])

for seed in range(24980000000, 25020000000):
    random.seed(seed)
    k = random.getrandbits(l)
    flag = flag_int ^ k
    flag = long_to_bytes(flag)
    if flag.startswith(b'flag{'):
        print('[+] seed:', seed)
        print('[*] flag:', flag.decode())
        break

実行結果は以下の通り。

[+] seed: 25000212790
[*] flag: flag{c3ntr4l_l1m1t_th30r3m_15431008597}
flag{c3ntr4l_l1m1t_th30r3m_15431008597}

squeal (web)

SQLインジェクション。以下のように入力し、[Submit]をクリックすると、フラグが表示された。

Username: admin' or 'A
Password: a (任意)
flag{squ34l_n0t_sql}