防衛省サイバーコンテスト2024 (Feb.) writeup

参加して430ptで10位。前回は16位だったのでちょっと上昇。
ギリギリ10位に入ったのでTop10のグラフに名前が残った。

Welcome

Welcome! [10 pt, 313 solves]

今回はOpenVPNで接続してその先のサーバにアクセスする形式だったため、この問題でOpenVPNの設定ファイルが配られていた。

OpenVPNは2.5以前と2.6でちょっと違いがあるらしく、2.5の設定ファイルを2.6で使うには互換性オプションをセットしないといけない。過去はまったことがあった記憶がある。今回は事前の案内にそのあたりが書かれていて親切。

FLAG: flag{WelcomeToMODCyberContest!}

Crypto

Information of Certificate [10 pt, 284 solves]

証明書のCNを答える問題。証明書ビューワの全般タブからはCNをコピーできないので、詳細タブからコピーする。

FLAG: flag{QRK7rNJ3hShV.vlc-cybercontest.invalid}

Missing IV [20 pt, 80 solves]

IVが不明ということだが、実はCBCモードの場合IVがわからなくて困るのは最初のブロックだけで、以降のブロックは問題なく復号できるのである。以下の図を参照。2番目以降のブロックの復号に必要なのはCiphertextのみであることがわかる。

というわけで強引に復号してやる。

見えてる文字列をキーワードに調査すると、OpenDocumentFormatらしいことがわかる。そしてその実態はMicrosoft Officeのファイルと同様にZipファイルらしいので、適当なZipファイルから頭16バイトをコピペする。
ZIPの仕様を日本語でまとめる · GitHub にファイルフォーマットがあるので16バイト目までを見てみると、16バイト目まではどのファイルでも変わらない情報と適当な値を入れてあっていい情報(CRCは合ってなくてもよいのだ)しかないのでコピペでうまくいくというわけである。

(Libre/Open)Officeは入れてなかったのでZipとして展開して中を探っていたらフラグがあった。
なお、zipgrepでdec_NoIV.binをgrepするだけでも出てきた。圧縮されたデータ部分は欠損してないので頭16バイトがおかしくてもgrepできるのね。

FLAG: flag{ESYQV0fPMxz4wMmU}

Short RSA Public Key [20 pt, 53 solves]

公開鍵と公開鍵で暗号化されたデータが与えられる。まずは公開鍵を見てみる。

Modulusが256bitと短い。短いと何が起こるかというと素因数分解ができてしまう。
16進で表示されているModulusを10進に変換してfactordbに突っ込むとこのように素因数分解できてしまうのである。

RSA Cipher Calculatorに暗号文、公開鍵の情報に加えて素因数分解してわかったpとqの値を入れて復号してやるとフラグが出てくる。

FLAG: flag{X0Myx6IHI8}

Cryptographically Insecure PRNG [30 pt, 22 solves]

線形合同法で生成された疑似乱数と平文のXORを取ったデータが与えられる。平文のヒントはASCIIであることと最初の4文字は英字であること。
最初の4文字のパターンは52 * 26 * 26 * 26 = 913,952通り(1文字目を大文字と仮定すればさらに半分)しかないので総当たりでASCIIコードのみが出てくるパターンを探ればよい。

# x_{n+1} = (233 x_n + 653) mod 4294967296

x = 4294967295

data = open("PRNG.bin", "rb").read()

a1 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
a2 = "abcdefghijklmnopqrstuvwxyz"

max_i = 0
for c1 in a1:
    for c2 in a2:
        for c3 in a2:
            for c4 in a2:
                xor_1 = ord(c1) ^ data[0]
                xor_2 = ord(c2) ^ data[1]
                xor_3 = ord(c3) ^ data[2]
                xor_4 = ord(c4) ^ data[3]

                result = c1 + c2 + c3 + c4
                x = xor_1 + (xor_2 << 8) + (xor_3 << 16) + (xor_4 << 24)
                next_x = (233 * x + 653) % 4294967296

                i = 1
                while True:
                    next_xor_1 = next_x & 0xff
                    next_xor_2 = (next_x & 0xff00) >> 8
                    next_xor_3 = (next_x & 0xff0000) >> 16
                    next_xor_4 = (next_x & 0xff000000) >> 24

                    next_c1 = next_xor_1 ^ data[i * 4]
                    next_c2 = next_xor_2 ^ data[i * 4 + 1]
                    next_c3 = next_xor_3 ^ data[i * 4 + 2]
                    next_c4 = next_xor_4 ^ data[i * 4 + 3]

                    if next_c1 < 0x20 or next_c1 > 0x7e or next_c2 < 0x20 or next_c2 > 0x7e or next_c3 < 0x20 or next_c3 > 0x7e or next_c4 < 0x20 or next_c4 > 0x7e:
                        break

                    result = result + chr(next_c1) + chr(next_c2) + chr(next_c3) + chr(next_c4)
                    next_x = (233 * next_x + 653) % 4294967296
                    i = i + 1
                    if max_i < i:
                        max_i = i
                    if i == 208:
                        print(result)
                        exit()

実行すると出力の中にフラグがある。

FLAG: flag{QVFE5i5LkZdR}

Forensics

NTFS Data Hide [10 pt]

NTFS~の問題は全て配布されたNTFS.vhdを使う。FTK Imagerで直接読んでもいいけど、読み取り専用にしてマウントしてそのドライブをFTK Imagerで読むのがいいと思う。

NTFSでデータを隠すと言ったらAlternate Data Stream。dirコマンドの/rスイッチを使うとAlternate Data Streamを表示できる。

あとは中身を表示してやればよい。

引用符内の文字列をBASE64デコードするとフラグが得られる。

FLAG: flag{data_can_be_hidden_in_ads}

NTFS File Delete [10 pt]

NTFSFileDeleteフォルダをFTK Imagerで見れば一発。

FLAG: flag{resident_in_mft}

HiddEN Variable [20 pt]

これとMy Secretはメモリダンプを解析する問題。

問題名の大文字を見るとENVになっているので環境変数を確認する。私はKaliにvolatility 3を導入してやった。

FLAG環境変数に謎の文字列が入っていることがわかる。これが答えかと思いきや違う。BASE64エンコードされてるのかと思いきや違う。CyberChefのMagicにかけたらBASE58と出た。ここだけなぜ…

FLAG: flag{volatile_environment_variable}

NTFS File Rename [20 pt]

NTFSFileRenameフォルダにあるRenamed.docxのリネーム前の名前は?という問題。このあたりの情報はジャーナルを漁ると出てくる。こんなコマンドを実行する。

出力されたCSVを見てみると、リネーム前のファイル名とリネーム後のファイル名が書かれている部分があることがわかる。

FLAG: flag{journaling_system_is_powerful}

My Secret [30 pt]

またメモリダンプの問題。今度は問題名でのヒントはないので、コマンドラインあたりに何かあるかなと見てみる。

運よく一発で引き当てた。このSecrets.7zが取得できればよいのでやってみる。

なんかErrorって出てるけどそっちの方もファイルは出力されている。コマンドラインにあったパスワードを使って展開するとSecrets.rtfが出現する。
開くと特にフラグは書かれていないように見えるが、実は2行目に白文字で書いてある。どうしてこういうしょうもないことするの。

FLAG: flag{you_cannot_find_this_secret!}

Miscellaneous

Une Maison [10 pt]

画像の中ほどに白と黒の縞々があるので、これはバーコードでは?と思い適当なアプリをインストールして読ませてみたらフラグが出てきた。

FLAG: flag{$50M!}

String Obfuscation [10 pt]

与えられたコードをいじってKEY変数をprint()してやって実行時の引数に渡してやればフラグをゲット!とやったんだけど、単純にFLAG変数をprint()すればよかった。

FLAG: flag{3FxYFm4uTYDFFzmb3}

Where Is the Legit Flag? [20 pt]

難読化されたPythonスクリプトが与えられる。ただしexec()で実行しておりその部分は難読化されていないため、exec()の引数を抜き出してprint()してやればどんなコードが実行されるかがわかる。fakeflag.pyの最後に実行されているのは以下のコード。

b'exec(zlib.decompress(base64.b64decode(TANAKA)))'

またexec()が出てきたので中身をprint()すると以下。

# Than volleyball vanish against lumpy berry.
SATO = '[QI3?)c^J:6RK/FV><ex7#kdYov$G0-A{qPs~w1@+`MO,h(La.WuCp5]i ZbjD9E%2yn8rTBm;f*H"!NS}tgz=UlX&4_|\'\\'
# Above face explain for physical decision.
# Via snake name round terrific brass.
# Following suggestion sound regarding female recess.
# Toward vessel disagree beneath huge porter.
SUZUKI = [74-0+0,
        87*1,int(48**1),
# Off purpose land as rural statement.
        int(8_3),int(32.00000),int('34'),
        76 & 0xFF,72 | 0x00,79 ^ 0x00,[65][0],
# During knot rely save wretched scarecrow.
        (2),47 if True else 0,int(12/1),10 % 11,ord(chr(26)),
        30+5,int(48/2*2),9*9]
#  Plus toe settle with vast insect.
#  Save hands shelter with ratty produce.
#  Outside legs nest versus tranquil relation.
#  As walk pat round rightful advice.
# Beside payment train by large key.
# Past behavior post toward unable home.
#  Among place complain considering unknown current.
( #  Around spark scorch above spotty grape.
    ''#  Underneath jewel chop past dependent rifle.
    .    join                          ([
        #  Since cobweb tie off hurt string.
SATO[i]         #  Since cobweb tie off hurt string.
for i in SUZUKI
        # if i > 4728:
        #     break
        # t = 234667 * 83785
        # print(t/3457783)
#  Through queen dam of slippery comparison.
])
#  By wall stroke without secret wash.
)
#  Opposite yoke need beside superb lumber.
print("flog{8vje9wunbp984}")

コメントが邪魔っけなので取り除いて整形するとこんなコード。

SATO = '[QI3?)c^J:6RK/FV><ex7#kdYov$G0-A{qPs~w1@+`MO,h(La.WuCp5]i ZbjD9E%2yn8rTBm;f*H"!NS}tgz=UlX&4_|\'\\'
SUZUKI = [74-0+0,
        87*1,int(48**1),
        int(8_3),int(32.00000),int('34'),
        76 & 0xFF,72 | 0x00,79 ^ 0x00,[65][0],
        (2),47 if True else 0,int(12/1),10 % 11,ord(chr(26)),
        30+5,int(48/2*2),9*9]
(''.join([ SATO[i] for i in SUZUKI ]))
print("flog{8vje9wunbp984}")

最後のprint()は偽データを表示してるだけなので無視して、その上の行をprint()してやるとフラグが出てくる。

FLAG: flag{PHmN2ILK6vsa}

Utter Darkness [20 pt]

与えられたdarkness.bmpを画像ビューワで見てみると真っ黒。ろくに考えずこれはステガノグラフィーに違いない!と思ってAperi’Solveに画像を突っ込んでみるも何も出ない。ちなみに私より前に突っ込んでる人がいた。

では別のツールで、ということでうさみみハリケーン付属の青い空を見上げればいつもそこに白い猫で読み込んでいろいろいじってみる。バイナリデータ視覚化表示で折り返し幅をいじったりしてたら何か出てきた。

上下反転して拡大してみるとフラグ文字列っぽいが、一部不明な文字や候補が複数ある文字があった。

何パターンか試してみてダメで、総当たりするのもつらいのでこのやり方は諦めて別の方法を考えることにした。

初心にかえってexiftoolで確認するとこんな感じ。

Bit Depthが1なので、1ピクセルあたり1ビットで表現されている。データを見るとff以外のデータもあることがわかるので、白と黒がそれなりに混ざっているはずだが画像はそうなっていない。

BMP ファイルフォーマットを参照すると、1bit Bitmapの場合の各ピクセルの色はパレットを参照することになっている。1bit Bitmapの場合パレットは2種類で0x36~0x390x3a~0x3dにある。darkness.bmpのパレットデータを見るとどちらも#000000になっている。このために画像が真っ黒になっていた。

0x36~0x39#ffffffに書き換えてやって画像ビューワで見てみるとフラグが得られた。

FLAG: flag{YjM5MDUyYzAxMj}

Serial Port Signal [30 pt]

与えられたCSVファイルを見ると20マイクロ秒ごとのH or Lが記録されている。ボーレートがどれくらいかは不明だが、ざっと見たところ0や1が5個か6個で1かたまりになっているように見受けられる。CSVからビット部分だけ抜き出した後大部分を手動で整形してビット列を5か6個ごとにまとめればビットシーケンスがわかって解読できるーーーー

と思いきやパリティの存在とかMSBからじゃなくてLSBから送ってることに気づいてなくて結局解けなかった。

Network

サーバが閉じられてしまってスクショを取ってないのでこの辺だいぶ適当。

Discovery [10 pt]

ポートスキャンすると80番が開いているのでアクセスしてみると変なドメイン名のサイトにリダイレクトされてしまう(そんなドメインないので繋がらない)
dirbusterとかやってもリダイレクトされる様を見て悩んでいたが、ふと「これはIPアドレスでアクセスされた時にFQDNでアクセスするようにリダイレクトしているのでは?」と思いついて/etc/hostsにそのドメイン名のエントリを追加してアクセスしてみたらリダイレクトされずにアクセスできた。

ディレクトリスキャンをすると/cmsadmin/ftpが見つかり、/ftp内にユーザ名とパスワードが書かれたファイルがあるので/cmsadminに行ってその情報を入れるとログインできる。
ログイン後、メニューからシステム情報を探すと求めている情報が得られる。

FLAG: flag{9.2.2.0, Revision: 14877}

FileExtract [10 pt]

pcap内のアプリケーションプロトコルはFTPのみなのでFTPでフィルタするとこんな感じ。

s3cr3t.zipというファイルを転送しているので、Wiresharkのエクスポート機能で取り出してやる。

展開しようとしてみるとパスワードがかかっていることがわかる。ここで、Wiresharkの画面に戻ってよく見てみると、ユーザがanonymouseで(anonymousではない)パスワードがbr2fWWJjjab3であることがわかる。このパスワードを使ってzipファイルが展開できて、中のファイルにフラグが書いてある。

FLAG: flag{6qhFJSHAP4A4}

Exploit [20 pt]

DiscoveryでこのCMSがwebEditionというものであることがわかったので、ExploitDBで調べると、使えそうなRCEが見つかった。
しかし、これを試そうと思っても重くて新規ページ作成もままならず、さらに5分でリセットされるという鬼仕様のため全然できないまま終了。

DO_tHe_best [20 pt]

名前の大文字部分を抜き出すとDOHとなるので、DNS over HTTPSの問題であると推測できる。ポートスキャンすると実際に443が開いていた。

自分でDNS over HTTPS (DoH)のリクエストを出したい – 1.1.1.1とかGoogle Public DNSとか – nwtgck / Ryo Otaを参考に1.1.1.1向けのクエリを出してみたらなんかうまくDoHできた感じ。
ただし、この後がわからなかった。いろいろ試してみてダメだった。st98氏のwriteupを見ると逆引きでいけたらしい。逆引きは試していたんだけど、d.b.c.a.in-addr.arpaにしなきゃいけないことをすっかり忘れていた。コマンドじゃなくて実際流れるプロトコルを理解することは大事だね。

Pivot [30 pt]

与えられたログイン情報でSSHログインすると、ホームディレクトリにsecret.txtがあるが、パーミッションが無く読めない。
Setuidされたバイナリを探すとbase64があったのでこれでBASE64エンコード→デコードしてファイルの中身を確認できる。
中にはMariaDBのクレデンシャルが書いてある。はて、MariaDBはどこにあるのか。nmapで探したいがSSHログインした先にはnmapが入ってない。SSHはダイナミックフォワーディングができるのでそれ経由でうまいことできないかと探していたらProxyChainsを使えばできるという情報を見つけた。便利なので覚えておこう。

SSH経由のポートスキャンで見つけたMariaDBに取得したクレデンシャルでログインしてデータベース内を漁るとフラグが見つかった。

なお、Pure bash TCP portscanというのもあるらしい。これも便利なので覚えておこう。

FLAG: flag{p!V071ng_M31s73r}

Programming

Logistic Map [10 pt]

そのまま計算するだけ。

i = 0
r = [0] * 10000
r[0] = 0.3

for i in range(9999):
    r[i+1] = 3.99 * r[i] * (1 - r[i])

print(r[9999])
FLAG: flag{0.8112735}

Randomness Extraction [20 pt]

ググったらコードが出てきた。ここのvon_neumann.pyを使っただけ。プログラミングしてない。とても良くないですね。出力ファイルをstringsにかけると中にフラグがある。

FLAG: flag{3TcPs8QFcX}

XML Confectioner [20 pt]

条件を満たすものを探すコードを書く。もうちょっとスマートな書き方があるのかもしれないけどストレートフォワードに。

import xml.etree.ElementTree as ET

tree = ET.parse("sweets.xml")
orders = tree.getroot()

for batch in orders:
    sweets = [ 0, 0, 0 ] # candy, cookie, icecream
    icecream_amount_min = 9999999
    candy_weight_sum = 0
    candy_shape = set()
    cookie_condition_meet = False
    for child in batch:
        if child.tag.endswith("candy"):
            sweets[0] = sweets[0] + 1
            weight = child.attrib["{http://xml.vlc-cybercontest.com/candy}weight"][:-1]
            shape = child.attrib["{http://xml.vlc-cybercontest.com/candy}shape"]
            candy_weight_sum = candy_weight_sum + float(weight)
            candy_shape.add(shape)
        if child.tag.endswith("cookie"):
            sweets[1] = sweets[1] + 1
            radius = child.attrib["{http://xml.vlc-cybercontest.com/cookie}radius"][:-2]
            kind = child.attrib["{http://xml.vlc-cybercontest.com/cookie}kind"]
            if kind == "icing" and float(radius) >= 3.0:
                cookie_condition_meet = True
        if child.tag.endswith("icecream"):
            sweets[2] = sweets[2] + 1
            amount = child.attrib["{http://xml.vlc-cybercontest.com/icecream}amount"][:-1]
            if float(amount) < icecream_amount_min:
                icecream_amount_min = float(amount)
            
    if sweets[2] >= 2 and icecream_amount_min >= 105 and candy_weight_sum > 28.0 and len(candy_shape) >= 5 and cookie_condition_meet == True:
        maximum_cookie_radius = 0
        maximum_cookie_flag = ""
        for child in batch:
            if child.tag.endswith("cookie"):
                radius = child.attrib["{http://xml.vlc-cybercontest.com/cookie}radius"][:-2]
                if float(radius) > maximum_cookie_radius:
                    maximum_cookie_radius = float(radius)
                    maximum_cookie_flag = child.text

        print(maximum_cookie_flag)
FLAG: flag{sZ8d5FbntXbL9uwP}

Twisted Text [30 pt]

またもやプログラムを書かずに解いた。正しく書ける自信がなかったので。

CLIP STUDIO PAINTが渦巻き変形できるとのことなのでこれを使って逆変換してやればわかるんじゃねーのというたくらみ。

元のTwisted.png
変形してみたの図

パラメータの意味を全然わからないまま適当にいじってたらフラグの最初と最後が見えたので間を読み取って入力したら正解だった。

FLAG: flag{LHZGhq3WTXvo}

Trivia

The Original Name of AES [10 pt]

Wikipediaを見れば書いてある(スペルが不安だったので確認した)

FLAG: flag{Rijndael}

CVE Record of Lowest Number [10 pt]

いろいろググってみるもなかなかピンポイントの情報が見つからず、MITREのCVEのページで全データを取得(全データなのでサイズがでかい。注意)することでやっと確認できた。

FLAG: flag{ip_input.c}

MFA Factors [10 pt]

これは暗記問題。所持じゃなくて所有とするケースもあるんだけどそれは通ったのだろうか。

FLAG: flag{所持・生体・知識}

Web

Browsers Have Local Storage [10 pt]

指定されたサーバにアクセスしてブラウザのDeveloper Toolsを開きLocal storageを参照するとフラグがある。

FLAG: FLAG{Th1s_1s_The_fIrst_flag}

Are You introspective? [10 pt]

GraphQLのエンドポイントを見つけられなかった。使ったwordlistが良くなかったみたい。目的に応じた適切なwordlistの選択は重要。

Insecure [20 pt]

自分のプロフィールを見るにはshow_profile.php?id=IDにアクセスするが、その際profile_success.phpにリダイレクトされてそこでプロフィールが表示される。
一方他人のプロフィールを見ようとするとprofile_error.phpに飛ばされて怒られる。
見たいのはid=0のユーザのプロフィールなので、show_profile.php?id=0にアクセスしてからprofile_success.phpにアクセスしたらフラグが表示された。

FLAG: FLAG{1qaz7ujmbgt5}

Variation [20 pt]

手を付けられず。

Bruteforce [30 pt]

与えられたソースコードを見るとtestユーザのパスワードがtestであることがわかる。adminユーザのパスワードは伏せられててわからない。JWTのキーもわからない。
testユーザでログインしてみると、JWTが返ってくる。さて次に何をするか。JWTの中でtestとなっている部分をadminにしてみても署名のキーがわからないのでJWTを生成できない。強引に間違った署名にしたり、algnoneにしてみてもダメ。
正解は題名の通りBruteforceすること。JWTをファイルに保存してrockyou.txtを使ってJohnにかけるとクラックできて、JWTのキーがconankunであることがわかる。

ユーザをadminにして生成したJWTを使ってアクセスすることでシステム内のファイルを読めるようになる。しかし/proc/self/cmdlineなどいろいろとファイルを読んでみても何もわからない。わからないまま時間終了。

正解は/proc/<PID>/cmdlineを読むことだったらしい。Dockerで動いているのでプロセスIDの予測は十分できるのこと。なるほど。予測できなくても数千くらいだったら総当たりしてもわかったかも。そうか、ここもBruteforceか。

感想

前回に引き続きちょうどいい難易度の良質な問題が多かったと感じる。今後も同じくらいのクオリティで続けて欲しい。いつか全問解けるようになりたいな。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください