Attack All Around

今やCTFと競プロばかりだが、トビタテ生のアメリカ留学やTOEFL奮闘記

DownUnderCTF 2021 writeup

久しぶりに満足する結果となりました!

今回は珍しくcrypto以外もちょこっと解けました。


DownUnderCTF 2021







Result



f:id:partender810:20210926191424p:plain
2000点いきたかった



Writeup


misc


The introduction

Are you ready to start your journey?
nc pwn-2021.duc.tf 31906

ncしてテキトーに名前を打ってしばらくすると、本当にハッカーか?と聞かれ、yesと答えるとflagが手に入ります。


DUCTF{w3lc0m3_70_7h3_duc7f_7hund3rd0m3_h4ck3r}



Discord

How about you visit our help page? You know what they say when you need 'help' discord is there for support :)

request-supportチャンネルにありました。最近のDiscord問題、本当にflagの場所が分からない…。


このチャンネルは、のちにめちゃくちゃ使います。チケットを発行すると、スタッフに質問できるようになります。どうなっているんだろう。


DUCTF{if_you_are_having_challenge_issues_come_here_pls}



General Skills Quiz

QUIZ TIME! Just answer the questions. Pretty easy right?
nc pwn-2021.duc.tf 31905

クイズを解いていくわけですが、30秒以内ということで手作業では間に合いません。

1) 1+1=? : 2
2) 16進数を10進数に : int("xxxx",16)
3) ASCIIコードに : ord(xx)
4) URLをASCIIに : urllib.parse.unquote("")
5) base64 decode : base64.b64decode(b"xxxx")
6) base64 encode : base64.b64encode(b"xxx")
7) rot13 : codecs("","rot-13")
8) rot13 : codecs("","rot-13")
9) 2進数を10進数に : int("xxxx",2)
10) 10進数を2進数に : bin(xx,2) "0b"を付ける
11) 世界一のCTFは? : "DUCTF"

4), 10) で苦労しました。


DUCTF{you_aced_the_quiz!_have_a_gold_star_champion}



rabbit

Can you find Babushka's missing vodka? It's buried pretty deep, like 1000 steps, deep.
ファイル:flag.txt

中身を見るとバイナリデータのよう。fileコマンドを叩くと

$ file flag.txt
flag.txt: bzip2 compressed data, block size = 900k

となり、拡張子が違うことが分かります。拡張子を".bz2"にして解凍しても、またバイナリデータとなりました。問題文にあるように、約1000回くらい解凍しないといけないようなのでプログラムを書いたのですが、途中でbzip2以外の拡張子も使わないといけないことが分かりました。


調べたところ、「.bz2, .xz, .gz, .zip」の4種類が使われているだろうということで、subprocessモジュールを使って解凍していきます。fileコマンドを叩いて、その結果で拡張子をmvコマンドで変えて、該当コマンドで解凍する流れです。zipファイルの時はややこしいのですが、解凍するとflag.txtと勝手に名前が決まっていますので、そこだけ注意しましょう。

import subprocess as sp

meirei = 0
# 0 : bz2
# 1 : xz
# 2 : gz
# 3 : unzip


for i in range(2000):
    print(i)
    if meirei == 0:
        sp.call(["bzip2","-d","temp.bz2"])
        #proc = sp.run("file temp", shell=True, stdout=sp.PIPE)
    elif meirei == 1:
        sp.call(["xz","-dv","temp.xz"])
    elif meirei == 2:
        sp.call(["gzip","-d","temp.gz"])
    elif meirei == 3:
        sp.call(["unzip","temp.zip"])
        sp.call(["mv","flag.txt","temp"])
        
    proc = sp.run("file temp", shell=True, stdout=sp.PIPE)
    if b"bzip2" in proc.stdout:
        sp.call(["mv","temp","temp.bz2"])
        meirei = 0
    elif b"XZ" in proc.stdout:
        sp.call(["mv","temp","temp.xz"])
        meirei = 1
    elif b"gzip" in proc.stdout:
        sp.call(["mv","temp","temp.gz"])
        meirei = 2
    elif b"Zip" in proc.stdout:
        sp.call(["mv","temp","temp.zip"])
        meirei = 3
    else:
        break


DUCTF{babushkas_v0dka_was_h3r3}




web


Cowboy World

I heard this is the coolest site for cowboys and can you find a way in?
https://web-cowboy-world-54f063db.chal-2021.duc.tf
Hint : https://www.youtube.com/watch?v=fn3KWM1kuAw

指定されたサイトに行くと、SQL injectionのようなログイン画面が出てきます。しかしusernameが分からない…。adminではないよう。

ヒントからrobots.txtだと推測。

# pls no look

User-Agent: regular_cowboys
Disallow: /sad.eml

URLの後ろに/sad.emlを付けると、emailをDLしました。

MIME-Version: 1.0
Date: Sun, 18 Jul 2021 20:48:32 +1000
Message-ID: <CAOXXCfPcb9Odey1va2xW=paWmwgrQoYFu9ayBUznwLr-FuD9Gw@mail.gmail.com>
Subject: :'(
From: DownUnder CTF <contact.downunderctf@gmail.com>
To: sadcowboys@everyone.com
Content-Type: multipart/alternative; boundary="0000000000000f998405c7639019"

--0000000000000f998405c7639019
Content-Type: text/plain; charset="UTF-8"

Everyone says 'yeee hawwwww'

but never 'hawwwww yeee'

:'(

thats why a 'sadcowboy' is only allowed to go into our website

--0000000000000f998405c7639019
Content-Type: text/html; charset="UTF-8"

<div dir="ltr">Everyone says &#39;yeee hawwwww&#39;<br><br>but never &#39;hawwwww yeee&#39;<br><br>:&#39;(<br><br>thats why a &#39;sadcowboy&#39; is only allowed to go into our website<br></div>

後ろの方に、「'sadcowboy'じゃないとウェブサイトに入れない」とあるので、usernameを'sadcowboy'にすれば良さそう。

password = ' OR 1=1 -- でログインできました。


DUCTF{haww_yeeee_downunderctf?}




reversing


nostring

This binary contains a free flag. No strings attached, seriously!
ファイル:nostrings

ghidraなどで解析すると、入力された文字列がflagの部分文字列かつ"D"から始まっているとcorrectと出ます。

Pythonからシェルコマンドを実行!subprocessでサブプロセスを実行する方法まとめ | DevelopersIO

このサイトで実行ファイルに引数を渡して、結果を得ます。

import subprocess as sp
import string
cmd = "./nostrings"
inp = b""

while True:
    for c in range(30,128):
    #print(c)
        v = chr(c)
        res = sp.run(cmd,shell=True,input=inp+v.encode()+b"\n",stdout = sp.PIPE)
        if b"correct" in res.stdout:
            print(v)
            #x = input("OK?> ")
            #if x == "n": continue
            inp += v.encode()
            print(inp)
            break


DUCTF{stringent_strings_string}




crypto


Substitution Cipher I

Just a simple substitution cipher to get started...
ファイル:substitution-cipher-i.sage, output.txt

flagの一文字ずつをordした値をxとすると、f = 13x2 + 3x + 7を計算し、chr(f)した値がoutput.txtに書かれます。

xを30から128くらいでbrute forceしてfを計算し、output.txtの文字をordした値と合致するか確認しflagを復元します。

msg = "REDACTED"
flag = ""
for c in msg:
    x = ord(c)
    for i in range(128):
        if x == 13*i*i+3*i+7:
            flag += chr(i)
            break
print(flag)


DUCTF{sh0uld'v3_us3d_r0t_13}



Substitution Cipher II

That's an interesting looking substitution cipher...
ファイル:substitution-cipher-ii.sage, output.txt

Iとは違い、数式fが分かりません。CHARSETが47文字なので、その中で計算が行われています。

「f = P.random_element(6)」はランダムに係数を決める6次式をfに格納します。6次式なので係数が7個必要です。brute forceするには477 = 1012 なので厳しいです。

ここで、mod 47での行列演算で係数を求めます。最初の6文字と最後の文字は"DUCTF{", "}"と分かっています。CHARSETでのindexはそれぞれ0~6なので、これらがfで計算されると[1,20,35,33,42,14,41]となることが分かります。

A = 7×7の正方行列。A_ij : pow(i,j,47)が格納。x^j 
B = 長さ7のvector。[1,20,35,33,42,14,41]
C = 係数
A×C = B
C = A_inv×B
n = 47
P.<x> = PolynomialRing(GF(n))

A = matrix(P, [[0, 0, 0, 0, 0, 0, 1],[1, 1, 1, 1, 1, 1, 1],[17, 32, 16, 8, 4, 2, 1],[24, 8, 34, 27, 9, 3, 1],[7, 37, 21, 17, 16, 4, 1],[21, 23, 14, 31, 25, 5, 1],[32, 21, 27, 28, 36, 6, 1]])
B = vector(P,[1,20,35,33,42,14,41])
Ainv = A.inverse()
print(Ainv*B) #[41, 15, 40, 9, 28, 27, 1]
from string import ascii_lowercase, digits
CHARSET = "DUCTF{}_!?'" + ascii_lowercase + digits
n = len(CHARSET)

x = [41, 15, 40, 9, 28, 27, 1]

def calc(val):
    ret = 0
    for i in range(7):
        ret += x[i]*pow(val,6-i)
    return ret%47

enc = "Ujyw5dnFofaou0au3nx3Cn84"
part_flag = "DUCTF{"
ct = ""
for p in part_flag:
    ct += CHARSET[calc(CHARSET.index(p))]
print(ct,enc[:6])

for i in range(len(enc)):
    candi = ""
    for c in CHARSET:
        if enc[i] == CHARSET[calc(CHARSET.index(c))]: candi += c
    print(i,candi,len(candi))

candiが複数になることもあり、長さが2,3のが一つずつありました。計6種類のflagが出てきたので全て提出してみたところ、一つだけ通りました。


DUCTF{go0d_0l'_l4gr4ng3}



Break Me!

AES encryption challenge.
nc pwn-2021.duc.tf 31914
ファイル:aes-ecb.py

flag + 任意のメッセージ + key が暗号化されます。ECBモードの同じ平文ブロックは同じ暗号文ブロックになるという脆弱性を利用します。任意のメッセージを調整して求めたい平文を特定するのですが、この形ではflagを直接求めることはできません。なので、keyを求めます。


任意のメッセージの長さを変えていくと、16文字にしたら暗号文の長さが変わったので、key長は16の倍数であることを踏まえるとflag長も16の倍数であることが分かります。そこから任意のメッセージを調整していって、keyを1バイトごと求めます。

任意のメッセージ = "0"×15+[1バイトbrute force]+"0"×15 とすると、"0"×15+[1バイトbrute force]がiブロック目になり、"0"×15+[keyの先頭1バイト]がその次のブロックになります。i, i+1ブロックが合致した時、keyの先頭1バイトが分かります。2バイト目以降は"0"の個数を減らして求めます。

kt = b''
for i in range(10,16):
    print("i =",i)
    for x in range(30,128):
        if x%10 == 0: print(x)
        read_until(f)
        sen = kt + long_to_bytes(x)
        #print(x,sen)
        sen = b"0"*(16-len(sen)) + sen + b"0"*(16-len(sen))
        if x == 130:
            print(sen)
        s.send(sen+b"\n")
        enc1 = read_until(f).strip()
        #print(x,enc1)
        try:
            enc = base64.b64decode(enc1.encode())
        except:
            print(enc1)
        
        e1 = enc[-48:-32]
        e2 = enc[-32:-16]
        #print(e1,e2)
        if e1 == e2:
            for j in range(0,len(enc),16):
                print(enc[j:j+16]," ",end="")
            print()
            print(f"Find! {i}th key chara")
            kt += long_to_bytes(x)
            print(kt)
            print(sen)
            break
read_until(f)
s.send(b"test\n")
enc = read_until(f).strip()
enc = base64.b64decode(enc.encode())
key = kt
cipher = AES.new(key, AES.MODE_ECB)
flag = cipher.decrypt(enc)
print(flag)


DUCTF{ECB_M0DE_K3YP4D_D474_L34k}



treasure

You and two friends have spent the past year playing an ARG that promises valuable treasures to the first team to find three secret shares scattered around the world. At long last, you have found all three and are ready to combine the shares to figure out where the treasure is. Of course, being the greedy individual you are, you plan to use your cryptography skills to deceive your friends into thinking that the treasure is in the middle of no where...
nc pwn-2021.duc.tf 31901
ファイル:treasure.py

FAKE_COORDSをlong_to_bytesすると、b'-31.95187912224053, 115.85957308693384' となります。この座標の形になっていれば関数is_coordsでFalseが返ることはありません。


この問題は2回ncする必要があります。1回目ではREAL_COORDSを求めます。

もらったshares[0]の値をそのままrun_combinerに送ると、返り値はsecret = REAL_COORDSとなります。

s1 = r1r2s mod p , s = RC
s2 = r1r1r2s mod p
s3 = r1r2r2s mod p

secret = s1^3 / s2s3 = s mod p

しかしshare[0]をそのまま送ると、仲間に宝物の座標がばれてしまうのでダメなようです。ゆえにもう一度ncします。


二回目の接続では、treasure.pyの45行目のif文を通るようshare[0]の値に1加えた値を送り座標の形にならないようにします。

次にもう一度run_combiner関数が呼ばれるので、今度はそこで返り値がFAKE_COORDSになるようにyour shareの値を計算します。

s = RC
s1 = share[0]*k = kr1r2s mod p とする
secret = (k^3)*s mod p
secret = FC となってほしいので
FC = (k^3)*RC mod p
FC/RC = k^3 mod p

d = inverse(3,p)
k = pow(FC*inverse(RC,p),d,p)

このように整数kを求め、share[0]と掛けた値をyour shareとします。そうすることで、返り値がFAKE_COORDSとなりflagがもらえました。


DUCTF{m4yb3_th3_r34L_tr34sur3_w4s_th3_fr13nDs_w3_m4d3_al0ng_Th3_W4y.......}



Secuchat

"With end-to-end encryption facilitated by military-grade RSA2048-OAEP and user-generated AES-256-CBC keys, rest assured that Secuchat has your conversations and secrets obscured from prying eyes."
Shame their DB got dumped. See if you can glean anything.
ファイル:secuchat.db

この問題は、先ほどのdiscordでいろいろ聞いて何とか解けました。しかも日本語が少し話せる方だったので、スムーズに色々聞くことができました。

与えられたデータベースの中身はこんな感じです。

sqlite> .schema Conversation
CREATE TABLE Conversation (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        initiator TEXT,
        peer TEXT,
        initial_parameters INTEGER,
        FOREIGN KEY (initiator) REFERENCES User(username),
        FOREIGN KEY (peer) REFERENCES User(username),
        FOREIGN KEY (initial_parameters) REFERENCES Parameters(id),
        UNIQUE(initiator, peer)
    );

sqlite> .schema Message
CREATE TABLE Message (
        conversation INTEGER,
        timestamp INTEGER,
        from_initiator BOOL,
        next_parameters INTEGER,
        encrypted_message BLOB,
        FOREIGN KEY (conversation) REFERENCES Conversation(id),
        FOREIGN KEY (next_parameters) REFERENCES Parameters(id)
    );

sqlite> .schema Parameters
CREATE TABLE Parameters (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        encrypted_aes_key_for_initiator BLOB,
        encrypted_aes_key_for_peer BLOB,
        iv BLOB
    );

sqlite> .schema User
CREATE TABLE User (
        username TEXT PRIMARY KEY,
        rsa_key BLOB
    );

Conversationテーブルでは誰と誰が会話するのか、Messageテーブルは実際の通信内容、ParametersではAES暗号についての情報、Userはrsaの鍵が入っています。


1. rsa_keyを取得

AESのkeyは暗号化されているようなので、まずはrsaの方を見ていきます。RSA2048-OAEPでピンときた方は問題ないですが、そうでなかった私はrsa_keyに入っているバイナリデータの意味が全然分かりませんでした。

RSA公開鍵のファイル形式とfingerprint - Qiita

このサイトで、PKCS#1方式ではないか?と予想がつきました。

from Crypto.PublicKey import RSA
keyPub = RSA.importKey(dat)
e = keyPub.e
n = keyPub.n

これで取得できます。


2. 素因数分解できるか確認

rsa_keyはUserテーブルにたくさんあります。どれもnは2048ビットなのでそのまま素因数分解はできないとは思ったのですが、共通素数があるのでは?とエスパー。全ペアを確認したところ唯一共通素数を使っているペアがいました。hortonashleyとeriksaundersでした。


3. encrypted_aes_key_for_xxx とは

AES-256-CBCを使ったと問題文にあります。encrypted_aes_key_for_xxx (initiator, peer どちらも)を見ると256バイトでした。AES-256-CBC256ビットのキーを使うので、何かしらの暗号化により256バイトになったと考えます。ここでRSAと組み合わせていることを考えると、AESのキーをRSAで暗号化したのでは?と推測できます。

  1. 素因数分解できたので秘密鍵が二つ手に入ります。encrypted_aes_key_for_xxxをすべて復号してみて32バイトになるのを探していきます。

ここで一つ問題があって、RSA2048-OAEPで暗号化しているので普段と同じように復号しても上手くいきません。モジュールを使います。

key_h = RSA.construct((p*q,65537,d1,p,q))
cipher1 = PKCS1_OAEP.new(key_h)
msg1 = cipher1.decrypt(para[1])


4. encrypted_message とは

Messageテーブルにあるencrypted_messageは暗号化される前のAESキーを使ってCBCモードで暗号化した文だと考えます。

  1. で復号して32バイトになったキーで、全てのencrypted_messageを復号します。ivはParametersにあるものをそのまま使います。

その中で唯一、"DUCTF"が含まれる平文がありました。


DUCTF{pr1m1t1v35, p4dd1ng, m0d35- wait, 3n7r0py?!}