🙆

i3CTF Writeup

2025/01/03に公開

はじめに

2024/12/20 19:00 ~ 2024/12/31 23:59に開催されたi3CTFに参加しました。
最終結果は2位で2D-Code、crib、ship、diff以外解くことができました。
本記事は、解いた問題のWriteupになります。

Misc

welcome (beginner)

welcome問題で問題文にFLAGが書かれています。

Word? (beginner)

docxファイルが与えられるのでまずは中身を確認しますが、何も書かれていません。
問題文の「Flag is hidden in this file」というヒントからunzipすることを考えます。

$ unzip word.docx

unzip後にファイルが大量に現れるので
全ファイルを対象にgrepコマンドでFLAGを探すと[Content_Types].xmlに書かれています。

$ find . -type f | xargs grep -e "FLAG*"

QR (beginner)

×マークが付けられたQRコードが与えられます。
そのままだと読み取れないので明るさを調整して読み取れるようにします。

music (beginner)

midiファイルが与えられるのでまずは再生してみます。
途中の音声が特殊なのでFLAG文字列が譜面になっていると推測して、
オンラインのmidiエディタで開くと譜面にFLAGが表示されます。
https://signal.vercel.app/

Icon (easy)

pptxファイルが与えられるのでまずは中身を確認しますが、
「flagはここにはないようだ...」と書かれています。

Word?と同様にunzipしてみます。

$ unzip icon.pptx

次に全ファイルを対象にgrepコマンドでFLAGを探しますが、見つかりません。

$ find . -type f | xargs grep -e "FLAG*"

画像に書かれていると推測してdocProps/thumbnail.jpegを開いてみます。
内容を見るとFLAGが書かれています。

crack (easy)

与えられたzipファイルにパスワードがかかっていて開くことができません。

問題文には「I thought I made the password easy...」と書かれているので
5文字以下の文字列をすべて試してみますが解凍できず。
次に辞書攻撃を考え、辞書としてrockyou.txtがメジャーなのでこちらを利用します。

import zipfile

def extract_zip(zip_path, password):
    try:
        with zipfile.ZipFile(zip_path, 'r') as zip_ref:
            zip_ref.extractall(pwd=password.encode('utf-8'))
            print(f"Password: {password}")
            return True
    except:
        return False

def solve():
    zip_path = 'crack.zip'
    rockyou_path = 'rockyou.txt'

    with open(rockyou_path, 'r', encoding='latin-1') as file:
        for line in file:
            if extract_zip(zip_path, line.strip()):
                return

if __name__ == "__main__":
    solve()

rockyou.txtの各行をパスワードとしてzip解凍を実施するプログラムを作成して実行します。
時間がかかりますが待つと解凍でき、出力ファイルを見るとFLAGが書かれています。

font (easy)

pngファイルが与えられるので、まずはexiftoolで情報を確認してみます。

$ exiftool image.png
(~略~)
Creator                         : xn--v9j2hwb4a4l2c
Rights                          : bnVtYmVyIG9mIHBpeGVscyBpbiB0aGUgaW1hZ2U=
(~略~)

そうするとCreatorとRightsに怪しげな文字列があります。
Creatorはpunycode、RightsはBase64でそれぞれエンコードされているのでデコードします。
https://www.dcode.fr/punycode-encoding

$ echo 'bnVtYmVyIG9mIHBpeGVscyBpbiB0aGUgaW1hZ2U=' | base64 -d

デコードすると以下のような文字列になるので、
パスワードは画像のピクセル数「765882」(=1091×702)ということがわかります。

Creator                         : パスワードは
Rights                          : number of pixels in the image

パスワードが必要になるということはsteghideかと推測して抽出を試みますが、
pngはサポートしていないようです。

$ steghide extract -sf image.png
steghide: the file format of the file "image.png" is not supported.

次に画像を確認してみます。
謎の絵文字が列挙されているだけでよくわからないのでGoogle画像検索をしてみます。
そうするとWingdingsというfontで書かれた文字列ということがわかります。

以下サイトでフォントを手打ちしてデコードします。
https://www.dcode.fr/wingdings-font

以下文字列にデコードされるのでサイトにアクセスしてみます。
https://forest.watch.impress.co.jp/library/software/stgngrapher/

ステガノグラファーというツールのダウンロードページだったので
ダウンロードしてpngファイルを開いてみます。

「透かしデータの抽出」を実行してexiftoolの情報から得たパスワードを入力すると
flag.txtをダウンロードすることができ、中身を見るとFLAGが書かれています。

rAndoMTexT (easy)

以下の謎の文字列のみが問題文に書かれています。

tIvolFLgqDzvIUnifAwfnenAtJskbNBMpAAEIxUXpeFPJbnepKPWBcmBkAIzGAShjJdxkoynfXBDehUtiDVTLjeFaUnRQJCEjREfNWRwqRaGkAaHlKVrOOoGjpUJfBSygOLotGcJuMrHomUbpCJZGNaX

まずはbase64でデコードしてみますが、上手くいかず。

次に小文字と大文字を別の文字で置き換えることを考えます。
文字列長を考えるとビット列になると思ったので実験してみます。
最初の8文字を小文字→0、大文字→1で置き換えてみると01000110(=0x46、F)になったので
全文字列をアスキー変換するとFLAGが現れます。

echo 'tIvolFLgqDzvIUnifAwfnenAtJskbNBMpAAEIxUXpeFPJbnepKPWBcmBkAIzGAShjJdxkoynfXBDehUtiDVTLjeFaUnRQJCEjREfNWRwqRaGkAaHlKVrOOoGjpUJfBSygOLotGcJuMrHomUbpCJZGNaX' | \
sed 's/[a-z]/0/g; s/[A-Z]/1/g' | perl -lpe '$_=pack"B*",$_'

sea (medium)

mp4ファイルが与えられるのでまずは再生してみます。
浜辺の動画ですが、波の音以外にドンドンという音が気になります。

ffmpegコマンドで音声を取り出して再度聞いてみます。

ffmpeg -i sea.mp4 -q:a 0 -map a sea.mp3

モールス信号のように聞こえたので
以下のサイトに先ほどの音声ファイルをアップして再生するとFLAGが出力されます。
https://morsecode.world/international/decoder/audio-decoder-adaptive.html

2D-Code (medium)

解けませんでした。
方針が立たなかったのでトライもしていません。

crib (hard)

時間内には解けませんでした。

与えられたzipファイルにパスワードがかかっていて開くことができません。
解凍する際に「Do you know about known plaintext attacks ?」というヒントが表示されます。

「plaintext attacks」でGoogle検索してみると既知平文攻撃というものが出てきます。
https://ja.wikipedia.org/wiki/既知平文攻撃

関連記事を見ているとpkcrackというツールを使うと解凍できることがわかります。

この攻撃ではzip内の既知ファイルが特定(用意)できると解凍できるようです。
まずはzipの中身を見ていくと「NSALibertyReport.p13.jpg」というファイルがあります。

$ zipinfo secret.zip
(~略~)
-rw-rw-r--  3.0 unx     1330 BX stor 24-Nov-22 10:12 flag.txt
-rw-r--r--  3.0 unx   191987 BX stor 24-Nov-21 16:44 NSALibertyReport.p13.jpg
(~略~)

「NSALibertyReport.p13.jpg」をGoogle検索すると同ファイルが見つかるのでダウンロードします。
https://ja.m.wikipedia.org/wiki/ファイル:NSALibertyReport.p13.jpg

次にpkcrackを実行してパスワードを外したzip(output.zip)の生成を試みます。

$ pkcrack -C secret.zip -c NSALibertyReport.p13.jpg -p NSALibertyReport.p13.jpg -d output.zip -a

上手くいったのでoutput.zipを解凍します。

$ unzip output.zip

flag.txtを取得できますが、中身を見てもFLAGがなく、文章と空白列があるのみです。

空白列が半角スペースとタブで構成されていたのでWhitespaceと思い、
実行してみてもコンパイルエラーとなり上手くいきません。
https://vii5ard.github.io/whitespace/

次にstegsnowを考えます。
stegsnowは半角スペースとタブを使ってASCIIテキスト内に文字列を隠すことができるツールです。
復元にはパスワードが必要で、問題文よりパスワードは'leona'であることが推測できます。
コマンドを実行するとフラグを取得することができます。

$ stegsnow -C -p 'leona' flag.txt

OSINT

island (beginner)

画像から島の名前を答える問題です。

まずはexiftoolを使って画像の情報を確認してみます。

$ exiftool island.JPG
(~略~)
GPS Latitude                    : 36 deg 15' 9.37" N
GPS Longitude                   : 136 deg 7' 14.56" E
(~略~)

緯度経度の情報が見つかったのでこれを使ってGoogleマップで検索すると島の名前がわかります。

Gyukaku (easy)

牛角の店舗を特定してその1つ下の階にある店舗の名前を答える問題です。

まずはexiftoolを使って画像の情報を確認しますが、有力情報は見つからず。
次に画像でGoogle画像検索をしてみますが、同店舗の画像は表示されず。
次にキーワード「牛角 4F」でGoogle画像検索をすると下の方に同店舗の画像が出てきます。
その画像に下の階の店舗も写っていたので店舗名を特定できます。

上記で見つからない場合は
牛角が4Fにある店舗を調べてローラーする作戦を考えていましたが、
画像検索で出てきたので回避できました。

Photographer (easy)

問題文としては、写真があり、この写真を撮った人の他の画像も見たいとのことです。
islandやGyukakuと違い、何を解答するのかがわかりません。

まずはexiftoolを使って画像の情報を確認してみます。

$ exiftool beautiful.jpg
(~略~)
Artist                          : promise_heavily_amongst
(~略~)

Artistに「promise_heavily_amongst」という文字列があり、
写真家の名前のように思えます。
試しに「FLAG{promise_heavily_amongst}」としましたが、誤答のようでした。

次に「promise_heavily_amongst」をGoogle検索してみます。
何もヒットしないので他検索エンジンでも同様に検索すると
bingでinstagramのユーザページがヒットします。

ページにアクセスするとプロフィール欄にFLAGが書かれています。

fuda (easy)

駒札が写った画像から寺の名前を答える問題です。

まずはexiftoolを使って画像の情報を確認しますが、有力情報は見つからず。
次に画像でGoogle画像検索をしてみますが、同じ駒札は見つからず。

画像の中にはいくつか重要なキーワードが含まれているので
それを使い、キーワード「仁王像 阿形 造形 平成 指定」でGoogle画像検索をすると
駒札が載っているブログが見つかります。
ブログの内容から寺の名前を特定することができます。

map (medium)

地図の画像から人が立っている場所のLV numberを答える問題です。

まずはexiftoolを使って画像の情報を確認しますが、有力情報は見つからず。
次に画像でGoogle画像検索をしてみると
チェコのProsečという地名であることがわかります。

次にLV numberが何かわからないので調べてみます。
チェコドメイン(.cz)のサイトにしか情報がなかったです。
https://cuzk.gov.cz/English/Cadastre-of-Real-Estate/Provision-of-data/Remote-Access/Outputs-from-KN-Provided-via-DP.aspx

このサイトによると所有権に関する番号ということがわかります。

土地からLV numberを求めるサイトを見つけられれば良いので探します。
「map czech」でGoogle検索すると以下のサイトが見つかり、
対応する場所で「What is here?」→「Information from Land Register」をクリックしていくと土地情報が表示されLV numberも書かれています。
https://en.mapy.cz/

ship (hard)

解けませんでした。
exiftoolや画像検索等してみましたが、何もわからなかったので諦めました。

Crypto

Julius (beginner)

問題文で以下のような文章が書かれています。

Qksec Tevsec Mkockb gkc k Bywkx qoxobkv kxn cdkdocwkx. K wowlob yp dro Psbcd Dbsewfsbkdo, Mkockb von dro Bywkx kbwsoc sx dro Qkvvsm Gkbc lopybo nopokdsxq rsc zyvsdsmkv bsfkv Zywzoi sx k msfsv gkb, kxn celcoaeoxdvi lomkwo nsmdkdyb pbyw 49 LM exdsv rsc kcckccsxkdsyx sx 44 LM. PVKQ sc OspqATNUTZwMIdT

最後のPVKQがFLAGをrotしたもののように見えるのでシーザー暗号で26パターン確認してみます。
http://www.net.c.dendai.ac.jp/crypto/caesar2.html

key=16で文章が復元でき、FLAGを見つけることができます。

xor (beginner)

chall.pyを確認すると、keyがstring.printable.strip()(印字可能文字列)の中で
1文字をrandom.choice()(ランダム)で選んでいるということがわかります。

keyは状態数が多くないので全探索します。
xorの結果は同じ値でxorを取ると復元でき、復号するとFLAGが求まります。

import string
import hashlib
from Crypto.Util.number import long_to_bytes

# output.txtの内容は省略する
enc = b''
hash = ''

# keyがランダムでわからないので全部試す
for key in string.printable.strip():
    flag = b''
    for i in range(len(enc)):
        a = enc[i] ^ ord(key)
        flag += long_to_bytes(a)

    if hashlib.blake2b(flag).hexdigest() == hash:
        print(f'key = {key}')
        print(flag.decode().replace("kore_ha_flag_desu_anata_ha_tadori_tuita_no_da_", ""))
        break

rsa (beginner)

chall.pyを確認すると、nが小さいので素因数分解をしてp、qを求めることができます。
p、qが求まるとeやcが既知なので
RSA暗号の復号式に当てはめると復号することができ、FLAGが求まります。

from Crypto.Util.number import *
from sympy import factorint

n = 89765553359668267846115148791526510167
e = 65537
c = 43726401623720020767763547639229741559

# 素因数分解をしてp, qを求める
factors = factorint(n)
p, q = list(factors.keys())

# 各種値を用いて復号する
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)

print(long_to_bytes(m).decode())

Matryoshka (easy)

問題名から何かが入れ子構造になっていると考えます。

与えられたファイルを見るとbase64でエンコードされているように見えるのでデコードします。

NjItPlU0TTJ3cFJER0NwNUI4cWtvdXdZaUhDVlF2a0lnZnRremh3NXBUemtZeDROWG96RENHV0J3MGZCQ0tVYWcxbmd3TFQzdHJHemswdmRQdjBSaVVDUkV2VUJSaWtZWHd4UnJNUkVLTW0xaXRaOUFMUXdvNXh6TVpBMU12MXM1VU81RmlTMFN5M1JURnUxOGZWQ2VkdEs2VEtOWllqcGFRWWswTkwycUhURGtCVU9nVGlGSHA3VWlydTJJZlV4QUJhMW5lM0p5c2QweDNTT0hEVGEyOVR5WGZrQW1ST2JjRHRYUGxHd2V4emZsc0N6Y2g1NVVSZ0ZQOWdNcXp2NjJGVGU3VHhLY3l0T05LQ2pKUjRYem1XNlA5blBGVDlwOHB2Smh6amFYSHlndzZHVXpLQkU1VU94cGNyZXpzZjhCNG1qeVVoYk9CWDNFdGVkVzRqWHpBZnFwblVSWFMxQTdYRWlpTTZxMk5VdE9iTHpMQ0lXVG9WOXBSVzM==
$ cat matryoshka.txt | base64 -d 
62->U4M2wpRDGCp5B8qkouwYiHCVQvkIgftkzhw5pTzkYx4NXozDCGWBw0fBCKUag1ngwLT3trGzk0vdPv0RiUCREvUBRikYXwxRrMREKMm1itZ9ALQwo5xzMZA1Mv1s5UO5FiS0Sy3RTFu18fVCedtK6TKNZYjpaQYk0NL2qHTDkBUOgTiFHp7Uiru2IfUxABa1ne3Jysd0x3SOHDTa29TyXfkAmRObcDtXPlGwexzflsCzch55URgFP9gMqzv62FTe7TxKcytONKCjJR4XzmW6P9nPFT9p8pvJhzjaXHygw6GUzKBE5UOxpcrezsf8B4mjyUhbOBX3EtedW4jXzAfqpnURXS1A7XEiiM6q2NUtObLzLCIWToV9pRW3

そうすると先頭に「62->」と付いた文字列が出力されます。
「62->」を除き、base62でデコードしてみます。
そうすると先頭に「58->」と付いた文字列が出力されます。

58->7nH1jTqufPSpfePjTvn8iLY1zrqZ7fkGFndTJ6BRpnwumrTN151mJJ8W33oEt5FrsdLohLGmzSYHQ2E6XdpHhtc6edKjgZHPLtq6oypWaayZzC6MFmVgRZ4bdp9JVUugzbbTy7VoEAks8QU9mXMW61yo3aHcMVP2uE3G5rpRrbgckrsrqeKa25jLo2yd6As2s527fJZJeEMXBKrTCbHas8UtW9d5mVXpxqPWk1fzBQCALqrns9Q9V96pfRCQHXR8p11EoBwhPFFJUNXD2SwG

同様に「58->」を除き、base58でデコードしてみます。
これをbase45、base32、HEXと実行していきます。

45->BL6HW5K09SL6AG6KIA 090M6HN9WNAT091N8T09GIAK09/F61C9 H9O098DBS09+H9309:F60C9WNAT09FH81C9+H94C9/F60C90JBS09TL6GY8GH8CC9*IB+NAJIAS09EG6X09GIA-B9*IBYY9IIA 09*IBX09EH8+09*IBIN9$H9T09GS8S09IH8G09TL66B8HX7
32->GE3C2PRXGU3TKMSEGNCTGNRTGE2DINJRGIYTGMJVG42EIMSEGNBTINJVIQ2DIMZZGM3DERBUIYZTSMRWGM2TGRRTIIZTMMRVGUZTGRBSGYZTKNJSGNDDGMBWGA3DA===
16->75752D3E363144512131574D2D3C455D4439362D4F3926353F3B3625533D2635523F306060

HEXをデコードするとuuエンコードされた文字列が出力されます。

uu->61DQ!1WM-<E]D96-O9&5?;6%S=&5R?0``

これをuuデコードするとFLAGを取得することができます。

baseデコードやuuデコードは以下のサイトを利用しました。
https://gchq.github.io/CyberChef/
https://www.dcode.fr/uu-encoding

pqrneca (easy)

chall.pyを確認します。

p、q、rを求めることで復号できますが、式が複雑なので整理します。
まず、rはビット数が小さいので全探索で値を特定できます。
次に以下の式に着目します。

a = pow(p + q + r, (p - 1) * (q - 1) * (r - 1), n) * ((p + q + r) % n)

一見複雑そうに見えますが、以下の部分はオイラーの定理により1になります。

pow(p + q + r, (p - 1) * (q - 1) * (r - 1), n)

それを適用すると、aは次式で表すことができます。

a = (p + q + r) % n

また、nについての式に以下の関係式もあります。

n = p * q * r

式変形すると以下の2式になります。

p + q = (a - r)
pq = n / r

a、r、nは既知なので、この2式を用いて二次方程式を解き、pとqの値を特定します。
以上より、p、q、rが全て求まるので
RSA暗号の復号式に当てはめると復号することができ、FLAGが求まります。

from sympy import symbols, solve
from Crypto.Util.number import long_to_bytes, inverse

# output.txtの内容は省略する
n = 
e = 
c = 
a = 

# rはビット数が少ないので全探索で求める
r = -1
for i in range(2 ** 15, 2 ** 16):
    if n % i == 0:
        r = i
        break

print(f'r = {r}')

p_times_q = n // r
p_plus_q = a - r

# 二次方程式でp, qを求める
x = symbols('x')
solution = solve(x ** 2 - p_plus_q * x + p_times_q, x)
p, q = [int(_) for _ in solution]

# 各種値を用いて復号する
phi = (p - 1) * (q - 1) * (r - 1)
d = inverse(e, phi)
m = pow(c, d, n)

print(long_to_bytes(m).decode())

fermat (medium)

chall.pyを確認すると、n = p*q、かつpとqが近い値であることがわかるので
フェルマー法を利用することができます。
√n辺りを確認するとpとqが求まるので
RSA暗号の復号式に当てはめると復号することができ、FLAGが求まります。

import gmpy2
from Crypto.Util.number import *

def fermat(n):
    a = gmpy2.isqrt(n) + 1
    b2 = gmpy2.square(a) - n
    while not gmpy2.is_square(b2):
        a += 1
        b2 = gmpy2.square(a) - n

    return int(a + gmpy2.isqrt(b2)), int(a - gmpy2.isqrt(b2))

# output.txtの内容は省略する
n = 
e = 
c = 

# フェルマー法を用いてp, qを求める
p, q = fermat(n)

# 各種値を用いて復号する
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)

print(long_to_bytes(m).decode())

pow (medium)

chall.pyを確認すると、eが小さいのでLow Public Exponent Attackが考えられます。
mod Nの影響を受けず、e乗根が求められるので暗号文を逆順に復号していけばFLAGが求まります。

from Crypto.Util.number import long_to_bytes, inverse
from sympy import integer_nthroot

# output.txtの内容は省略する
n = 
e = 
c = 

# 逆元
c_inv = inverse(c, n)

# e乗根
m = integer_nthroot(c_inv, e)
m = integer_nthroot(m[0], e)
m = integer_nthroot(m[0], e)

flag = long_to_bytes(m[0])
print(flag.decode())

yyy (hard)

問題文を見るとMatrixになっているのがヒントのように見えます。
chall.pyを確認すると、特徴的なアルゴリズムなので
「ctf crypto 増加列 gcd 行列」のようなキーワードでGoogle検索すると、
「Merkle-Hellmanナップサック暗号」という暗号アルゴリズムということがわかります。
LLLアルゴリズム(Lenstra, Lenstra, Lovasz)で復号するとFLAGが求まります。

from Crypto.Util.number import long_to_bytes
from sage.all import ZZ, Matrix

def is_valid_vector(b):
    if b[0] != 0:
        return False
    for i, x in enumerate(b):
        if i != 0 and abs(x) != 1:
            return False

    return True

# output.txtの内容は省略する
pubkey = []
ct = 

matrix_size = len(pubkey) + 1
m_list = [
    [0 for _ in range(matrix_size)] for _ in range(matrix_size)
]

for i in range(matrix_size - 1):
    m_list[i][0] = pubkey[i]
    m_list[i][i + 1] = 2
    m_list[matrix_size - 1][i + 1] = -1

m_list[matrix_size - 1][0] = -ct

llled = Matrix(ZZ, m_list).LLL()

flag_vecs = []
for basis in llled:
    if is_valid_vector(basis):
        flag_vecs.append(basis)

for v in flag_vecs:
    flag = ""
    for _bit in reversed(v[1:]):
        c = ("1" if _bit == 1 else "0")
        flag += c

    print(long_to_bytes(int(flag, 2)).decode())

コードは以下サイトを参考にしました。
https://project-euphoria.dev/blog/10-lll-for-crypto/

diff (expert)

解けませんでした。
lやl2の状態数が少ないので全探索をして、
Franklin-Reiter Related Message Attackをすれば解けると考えましたが、
時間が足りずにタイムアップでした。

Forensics

What is this file? (beginner)

何のファイルでしょう?という問題なのでfileコマンドで確認してみます。

$ file unknow

Wordファイルということがわかるので
拡張子を「.docx」にしてファイルを開くとFLAGが書かれています。

SVG (beginner)

SVGファイルが与えられるので開いてみるとQRコードが表示されます。
読み取ると「Extensible Markup Language」のWikipediaが表示されます。

ファイルの中身を見てということかと思ったので、image.svgの中身を見ると
<Flag>のcontentにPythonのコードのようなものが書かれていることがわかります。
全て抽出してpythonコードとして実行するとFLAGが出力されます。

grep -o 'content="[^"]*"' image.svg | sed 's/content="//;s/"//' | python3`

Liangmen (easy)

画像ファイルを開くと「Not in ZIP format」と書かれています。
zip内にないということはzip自体が答えと考えられるので、zipファイルをfileコマンドで確認します。

$ file liangmen.zip
liangmen.zip: PNG image data, 1310 x 730, 8-bit/color RGBA, non-interlaced

するとPNG imageということがわかるので、
拡張子をpngに変更して画像を表示するとFLAGが書かれています。

Concealment (medium)

問題文にパスワードが書かれています。
zipを解凍するために必要なものではなく、
パスワードが必要なものを考えるとsteghideが思い浮かんだのでコマンドを実行します。

$ steghide extract -sf concealment.jpg

そうするとsecret.txtが抽出でき、中身を見るとFLAGが書かれています。

※エスパーしたので正攻法ではないと思います。

Web

Meta (beginner)

問題リンクよりWebページを開き、開発者ツールでソースコードを見るとFLAGが書かれています。

login (easy)

問題リンクよりWebページを開くとログイン画面が表示されます。
SQLiできるかもしれないので以下の入力を試してみるとログインが成功してFLAGが表示されます。

ユーザー名:root
パスワード:'OR '1' = '1

input (medium)

問題リンクよりWebページを開き、開発者ツールでソースコードを確認します。
XSSの脆弱性がありますが、Write()の中で文字パターンを削除しているので
入力値は考えないといけません。
以下のページを参考に文字列を入力して「表示!」をクリックするとFLAGが表示されます。
https://blog.hamayanhamayan.com/entry/2021/12/07/231441

<img src="x" onerror="alert(0)">

dummy (medium)

問題リンクよりWebページを開くと偽FLAGが表示されます。
開発者ツールでソースコードを確認してみると
HTMLに「Where is style.css?」というコメントが書かれています。
ここでdummy.cssをstyle.cssに差し替えられればFLAGが取得できると考えます。

style.cssを探すとmain.wasmにそれらしきものが見つかります。

syncstyle.css\0a\0a.dummy:before{\0a  content: \22IREL\c2\80yN@I<{lV?6|\c2\86\22\0a}

\0aは改行なので「\22IREL\c2\80yN@I<{lV?6|\c2\86\22」がFLAG文字列と考えられます。
文字列のデコードはmain.jsで実施しているのでtextの値を置き換えるとFLAGが表示されます。

WebAssembly.instantiateStreaming(fetch("wasm/main.wasm"), go.importObject).then((result) => {
    // (~略~)
    const element = document.getElementById("dummy");
    let text = "\x22IREL\x80yN@I<{lV?6|\x86\x22";
});

ローカル環境に資材一式を持ってきて試しました。
最初上手くいかずに実験をしていると、
文字列中の\c2が不要ということがわかったので削除するとFLAGが表示されました。

login2 (hard)

問題リンクよりWebページを開くとログイン画面が表示されます。
ダメ元でloginと同じ入力を与えてみると偽FLAGが表示されます。

loginと同様にSQLiと仮定して調査を進めます。
まずはDBMSを特定するために、SQL文のコメントを変えて実行してみます。
入力値で#を末尾に付けると文として認識されたのでMySQLと判断できます。

MySQLではINFORMATION_SCHEMA.TABLESにテーブル情報が含まれているので
UNIONを利用してテーブル一覧を確認してみます。

' UNION SELECT GROUP_CONCAT(TABLE_NAME) FROM INFORMATION_SCHEMA.TABLES #

GROUP_CONCAT()を用いることで結果を連結して表示できます。
結果が1件分しか出力されないように制限されていたのでこのようにしています。

出力に文字数制限があるようでテーブル名がすべて表示されなかったのでWHERE句で対象を絞ります。

' UNION SELECT GROUP_CONCAT(TABLE_NAME) FROM INFORMATION_SCHEMA.TABLES WHERE CHAR_LENGTH(TABLE_NAME) < 10 #

そうすると、s3cretという怪しいテーブル名が出てきます。

(~略~),users,users,s3cret`  

パスワードに以下値を入力し、s3cretテーブルから情報を取得するとFLAGが表示されます。

' UNION SELECT * FROM s3cret; #

Network

http (beginner)

Wiresharkでpcapngファイルを開きます。
HTTPパケットを見るとtext dataにFLAGが書かれています。

Bob (easy)

Wiresharkでpcapngファイルを開きます。
パケットが大量にあるのでTCPストリームを確認します。
TCPストリームはTCPパケットを右クリックして、「追跡」→「TCPストリーム」で確認できます。

右下のストリームを進めていくと色々なデータのやり取りがあり、
その中にPNGファイルも含まれるように見えます。
FTPでやり取りをしているので
「ファイル」→「オブジェクトをエクスポート」→「FTP-DATA」をクリックして、
一覧に表示されたpngファイルを保存します。

保存したファイルを見るとFLAGが書かれています。

basic (easy)

Wiresharkでpcapngファイルを開きます。
Bobと同様にTCPストリームを確認します。
問題文からBasic認証であることがわかるのでAuthorizationヘッダを確認します。

Authorization: Basic Sm9objpGTEFHe0I0czFjXzFzX24wdF9zM2N1cjN9

Basicの後ろの文字列をbase64でデコードするとFLAGが出力されます。

$ echo 'Sm9objpGTEFHe0I0czFjXzFzX24wdF9zM2N1cjN9' | base64 -d

layout (easy)

問題文を見るとファイル構造のようなものが書かれているのでヒントと考えます。

Wiresharkでpcapngファイルを開きます。
通信のやり取りを見るとHTML/CSS/JavaScriptをGETしているので
Webページを表示しているように見えます。
Webページを復元するとFLAGが確認できると考えられるので復元してみます。
HTTPのやり取りのため、ファイルのエクスポートは
「ファイル」→「オブジェクトをエクスポート」→「HTTP」をクリックして、
一覧に表示されたファイルをすべて保存します。

問題文の形に合うようなフォルダ構成にしてHTMLファイルを開きます。

app
  |- index.html
  |- css
    |- style.css
    |- style2.css  
  |- js
    |- script.js

「GET」ボタンをクリックするとFLAGが表示されます。

ppap (expert)

問題タイトルからPPAPが関係していると考えます。

Wiresharkでpcapファイルを開きます。
最初にTCPストリームを見るとメールのやり取りが確認でき、
互いに秘密鍵を交換し、その後に暗号化されたデータのやり取りがされていることがわかります。

データを復号するために秘密鍵を抽出します。
quoted-printableでエンコードされているので併せてデコードします。

# pcapファイルから秘密鍵に対応する部分を抽出する
strings ppap.pcap | awk '/BEGIN PRIVATE KEY/,/END PRIVATE KEY/' > tmp.txt
  
# 2つあるのでcsplitで2ファイルに分割する
csplit -z -f private_ tmp.txt '/BEGIN PRIVATE KEY/' '{*}'

# 秘密鍵をデコードする
for i in private_0{0,1}; do python3 -m quopri -d < "$i" > "${i}.pem"; done

# 一時ファイルを削除する
rm tmp.txt private_00 private_01

秘密鍵をWiresharkに登録します。
「編集」→「設定」を開き、Protocolsより「TLS」を選択します。
「RSA keys list」の「編集」より、
IP address/Port/Protocol/秘密鍵のファイルパスを2人分入力します。

登録後に暗号化データが確認できるようになるのでTLSストリームを確認します。

そうするとzipファイル送付とパスワード送付のやり取りがあります。
※パスワードは後で使うのでメモしておきます。

zipファイルのデータはbase64でデコードします。
CyberChefを使い、デコードした結果を「download.zip」として保存します。

保存したzipを解凍する際にパスワードを求められるので、先ほどメモしたパスワードを使います。

$ unzip download.zip
Archive:  download.zip
[download.zip] secret.pcap password:

解凍するとsecret.pcapが出てくるのでWiresharkで開きます。
パケットを見るとH264のデータがやり取りされているのでエクスポートしてみます。
標準ではできないので以下の拡張を入れます。
https://github.com/volvet/h264extractor

H264としてエクスポートしたファイルのファイル名をinput.h264にリネームしておきます。
ffmpegコマンドでH264をmp4に変換します。

ffmpeg -i input.h264 -c:v libx264 -preset slow -crf 18 -c:a copy output.mp4

出力されたmp4ファイルを再生するとFLAGが確認できます。

Reversing

hello (beginner)

まずは与えらえたファイルをfileコマンドで確認しています。

$ file hello
hello: ELF 64-bit LSB pie executable, x86-64, version 1

実行できそうなので実行しています。

$ chmod +x hello
$ ./hello
FLAG{_is_not_here_^v^}
Flag is with i3ctf.

偽FLAGと「Flag is with i3ctf」というヒントが出力されます。

stringsコマンドでi3ctfが含まれる行を確認してみます。

$ strings hello | grep i3ctf

何も出力されず。
ファイル内にヒントがないかを探してみます。

$ strings hello | head
UPX!
tdoPC
/lib64

先頭に「UPX!」と出てきたのでupxを使いアンパックしてみます。

$ upx -d hello

再度stringsコマンドでi3ctfが含まれる行を確認してみます。

$ strings hello | grep i3ctf
Flag is with i3ctf.
i3ctf_F_is_here
i3ctf_L_is_here
(~略~)
i3ctf_g_is_here
i3ctf_s_is_here
i3ctf_}_is_here

i3ctf_~_is_hereの文字列が複数出力され、縦読みするとFLAGになります。

$ strings hello | grep i3ctf | awk -F '_' '{print $2}' | tr -d '\n'

pyc (easy)

pycはPythonのソースコードをコンパイルした結果のバイトコードで
uncompyle6コマンドでデコンパイルすることができます。

$ uncompyle6 -o . file.pyc
file.pyc -- 
# Successfully decompiled file

file.pyが生成されるので余計な部分を削除して実行するとFLAGが出力されます。

def a():
    return "3ZUMw"

def flag():
    import base64
    return str((base64.b64decode(f"Rkx{f()}WMwb{l()}fOHk{a()}ZEV{g()}")), encoding="utf-8")

def l():
    return "XAxbGV"

def run():
    win()

def f():
    return "BR3tkR"

def win():
    print(flag())

def g():
    return "9"

run()

Liar (medium)

まずはfileコマンドで何のファイルか確認してみます。

$ file shufexe.elf 
shufexe.elf: MS-DOS executable

shufexe.elf は厳密には「file shuf[U+202E]exe.elf」で
Unicodeの「RIGHT-TO-LEFT OVERRIDE」(⁠RLO)が埋め込まれているので
環境によっては「shuffle.exe」と表示されます。

MS-DOSの実行ファイルのため、DOSBoxを試してみましたが、
上手く実行できなかったのでobjdumpコマンドで逆アセンブルした結果を眺めてみます。

$ objdump -b binary -m i8086 -D shufexe.elf > out.txt

movw命令にFLAG文字列のような値が見つかります。

1624:	c7 45 a0 46 00       	movw   $0x46,-0x60(%di)
1629:	00 00                	add    %al,(%bx,%si)
162b:	c7 45 a4 4c 00       	movw   $0x4c,-0x5c(%di)
1630:	00 00                	add    %al,(%bx,%si)
1632:	c7 45 a8 41 00       	movw   $0x41,-0x58(%di)
1637:	00 00                	add    %al,(%bx,%si)
1639:	c7 45 ac 47 00       	movw   $0x47,-0x54(%di)
163e:	00 00                	add    %al,(%bx,%si)
1640:	c7 45 b0 7b 00       	movw   $0x7b,-0x50(%di)

全て抽出して整形するとFLAGが出力されます。

$ objdump -b binary -m i8086 -D shufexe.elf | grep movw | awk '{print $8}' | sed 's/,.*//' | xxd -r -p

Discussion