【CTF上級編|第3話/全10話】SSTIでテンプレートエンジンを乗っ取る|{{7*7}}から始まるRCE

⚡ CTF上級編|第3話/全10話

SSTIでテンプレートエンジンを乗っ取る|{{7*7}}から始ま&#x308BRCE

第2話では「サーバー&#x306BURLを信じせすぎる」問題(SSRF)を体験しました。第3話で扱うSSTI(サーバーサイドテンプレートインジェクション)は、「サーバーにユーザー入力を信じすぎる」問題の、さらに根深いバージョンです。文字列だと思って信じた入力が、実はコードそのものとして実行されてしまう——その恐ろしさを、安全に体験します。

🧨 SSTI(テンプレートインジェクション) 🧮 式評価エンジンの危険性 🔒 安全な自作パーサーで体験 ⭐ 難易度:★★★★☆
01

🧨 SSTIとは何か|テンプレートエンジンが「式」を評価してしまう

文字列だと思って信じた入力が、コードとして動く

SSTI(Server-Side Template Injection、サーバーサイドテンプレートインジェクション)は、Webアプリケーションが使う「テンプレートエンジン」が、ユーザーの入力を本来のデータとしてではなく、テンプレートの構文そのものとして解釈してしまう脆弱性です。Jinja2(Python)&#x30FBTwig(PHP)&#x30FBFreeMarker&#x30FBVelocity(Java)&#x30FBEJS(Node.js)など、サーバーサイドのテンプレートエンジンの多くは、{{ }}${ }のような記法で囲んだ部分を「式」として評価し、結果を画面に埋め込みます。

たとえば「ようこそ、{{ name }}さん!」というテンプレートがあり、nameにユーザーが入力した値を渡すと、通常は入力した文字列がそのまま埋め込まれます。ところが実装に問題があると、ユーザーが入力した文字列自体が「テンプレートの一部」として扱われてしまうことがあります。たとえば名前欄に{{7*7}}と入力すると、画面に「7*7」ではなく「49」と表示される——これは、エンジンが文字列ではなく式として評価している証拠です。この{{7*7}}は、実際のバグバウンティ報告書でも、SSTIの存在を確認する最初の一手として非常によく使われる、業界で広く知られた合い言葉(canary)です。

SSTIの基本構造 ユーザー入力 {{7*7}} テンプレート文字列に連結 「ようこそ、{{7*7}}さん」 これが新しいテンプレートになる エンジンが式として評価 表示:49 文字列の一部として済むはずが、構文として再解釈されてしまう
📌

この話の前提知識

上級編③は、実践編4話で学んだ「想定外の場所でユーザー入力が評価される」という発想をさらに進めます。プロトタイプ汚染は「オブジェクトの構造」が汚染される脆弱性でしたが、SSTIは「コードそのもの」が汚染される脆弱性です。

  • 実践編 第4話「プロトタイプ汚染でadmin権限を奔う」(「想定外の評価」という発想の連続性)
02

🧮 なぜ危険なのか|算術確認から本物のコード実行へ

発見と被害の間にあるのは数文字の差

{{7*7}}で確認できるのは「式が評価されている」という事実だけです。しかし本物の攻撃では、ここから一気に深刻な被害に発展します。Jinja2であれば{{ self.__init__.__globals__... }}のような構文でPythonの内部オブジェクトをたどり、最終的にOSコマンドを実行する関数(os.popen等)に到達できることが知られています。

エンジン言語確認用canary危険度の高い延長
Jinja2Python{{7*7}}self.__init__.__globals__経由でOS命令実行まで到達可能
TwigPHP{{7*7}}_self.env...経由でPHP関数呼び出しに発展
FreeMarkerJava${7*7}freemarker.template.utility.Executeでシステムコマンド実行
VelocityJava#set($x=7*7)$x$class.inspect(...)経由でクラスローダ悪用
⚠️

発見と被害の間にあるのは数文字の差

「画面に49と表示される」と「サーバー上で任意のOSコマンドが実行される」の間にあるのは、新しい脆弱性ではなく、同じテンプレートエンジンの構文を少しずつ深く掘っていくだけの作業です。だからそこ、SSTIは発見した時点ですでに深刻度高と判断するのが実務の慣例です。

03

🔒 今回のラボの安全設計|なぜeval()を使わないのか

安全に体験するための工夫

本話のラボは、実際&#x306BPythonやJavaScriptのテンプレートエンジンを動かしているわけではありません。eval()new Function()を使えば、ユーザーが入力した文字列を一瞬で「実行」できてしまいますが、それはまさに今学んでいる脆弱性そのものを、訪問者であるあなたのブラウザの中に作ってしまうことになります。

そこで本話のラボでは、四則演算と決められたプロパティへのアクセスだけを解釈する、自作の小さな「式パーサー」を実装しました。文字列を1文字ずつ読み取り、数字・演算子・識別子をトークンに分解し、決まった文法規則(再帰下降法)に従って組み立てる、という方法です。evalFunctionを一度も呼ばずに、{{7*7}}を本当に計算して「49」を返すことができます。

🛡️

実務でも同じ考え方が使われている

Jinja2にはSandboxedEnvironmentという、危険な属性へのアクセスを制限したサンドボックス実行モードが用意されています。本話の自作パーサーは規模も機能もずっと小さいものですが、「危険な操作の入口を最初から物理的に塞いでおく」という設計思想は、実務のサンドボックス実装とまったく同じです。本話のパーサーも、__proto__constructorといった名前のプロパティへのアクセスを明示的に拒否する、二重の安全層を入れています。

04

🗝️ 実践チャレンジ:表示名欄からテンプレートエンジンを乗っ取れ

name欄に式を投げて、画面の変化を確認しよう

架空の掋示板「やさい掋示板」には、表示名を入力すると「ようこそ、○○さん!」と表示する歡迎バナー機能があります。この機能は、入力された文字列をテンプレート文字列にそのまま連結してからテンプレートエンジンに渡しているため、name欄に{{ }}を含めると、その部分が新しいテンプレート構文として評価されます。

表示名を入力して送信してください。

例:{{7*7}} をそのまま試すとどうなるか確認してみましょう。

🧩 実践チャレンジ:2つのステージでテンプレートエンジンを乗っ取れ

ステー&#x30B81をクリアすると、ステー&#x30B82が解放されます。両方クリアした時点でスコアが確定します。

📊 ステージ進捗: 0/2|挑戰回数: 0回

1ステー&#x30B81:式評価を確認する

上の表示名欄に{{7*7}}と入力して送信し、画面の表示がどう変わるか確認してください。結果を使ってSSTI-〇〇形式で入力してください。

第3話の攻撃チェーン ステー&#x30B81:式評価確認 {{7*7}}→49 ステー&#x30B82:属性探索 config.secret_key 内部設定漏洩 secret_key=CTF符号 テンプレート文字列への激穋+制限のないプロパティアクセス=情報漏洩
05

🛡️ 防御側の視点|SSTIをどう防ぐか

根本の原则は「テンプレート構文に入力を混ぜない」

SSTIの根本原因は、「テンプレートの構文(文字列そのもの)」と「テンプレートに渡すデータ」を混ぜてしまうことにあります。

  • 構文とデータを分ける:テンプレート文字列自体は定数として固定し、ユーザー入力は必ず「値」として渡す(構文の一部にしない)
  • サンドボックス実行環境:Jinja2のSandboxedEnvironmentなど、危険な属性アクセスを事前に制限するオプションを有効化する
  • ロジックレスなテンプレートエンジン:Mustacheのように、式評価を一切行わず単純な値の差し替えだけに専化したエンジンを選ぶ
  • 入力検証&#x30FBWAF{{${のようなテンプレート構文を含む入力を検出する(補助的な対策)

最強の防御は「評価しない」こと

SSRFの防御が「許可リスト」だったのと同じで、SSTIの最強の防御は「そもそも評価しない」ことです。ユーザーが入力した文字列を新しいテンプレートとしてコンパイルし直す必要がなければ、SSTIの検証構文({{7*7}})そのものが成立しません。

06

📝 まとめ+FAQ+次回予告

テンプレートエンジン、乗っ取り完了

第3話では、テンプレートエンジンがユーザー入力を構文として評価してしまうSSTIを体験しました。次回はパディングオラクル攻撃という、「成功/失敗」だけの情報から暗号を解く手法を扱います。

✅ 今回のまとめチェック

・SSTIはユーザー入力がテンプレート構文そのものとして評価されてしまう脆弱性
{{7*7}}→49は存在確認の業界標準canary
・発見から本物のRCEまでの距離はエンジン固有の構文を深めるだけ
・本話のラボはeval/Functionを一度も使わず、自作の式パーサーだけで安全に再現
・最強の防御は構文とデータを混ぜない設計

Q. 自分のブラウザでこのラボを試して、本当に何かが実行されたりしませんか?

しません。本話のラボはeval()new Function()を一度も呼ばず、四則演算と決められたプロパティへのアクセスだけを解釈する自作のパーサーだけで動いています。どんな文字列を入力しても、あなたのブラウザ上で任意のJavaScriptが動くことはありません。

Q. 実際のSSTIも、最後は必ずOSコマンド実行まで到達するのですか?

そうとは限りません。エンジンの種類やサンドボックス設定の有無によって、到達可能な深刻度は変わります。ただし本話のパーサーのように「属性アクセスだけしかできない」場合でも、内部設定やキャー情報の漏洩だけでも十分に重大な侵害とみなされます。

Q. コード実行を伴わないテンプレートエンジン(Mustache等)にすれば完全に安全ですか?

大幅に安全になりますが、完全にはいきません。エスケープ処理の不備(XSS)や、値の中に再帰的にテンプレート構文を埋め込めてしまう実装ミスなど、別の問題が残る可能性はあります。

Q. 送信した表示名はどこかに送信されますか?

送信されません。入力した表示名も含めて、すべてブラウザ&#x306ElocalStorage(あなたの端末内)だけに保存され、外部のサーバーには一切送信されません。

次回.第4話

パディングオラクル攻撃|「復号成功/失敗」だけで暗号文を解く

CBCモードのパディング検証が漏らす情報量だけで平文を割り出す、本物のWeb Crypto APIを使った暗号攻撃を体験します。

📚 参考情報

  • &#x300CCTF上級編」第1話(信頼境界の考え方)・実践編第4話(プロトタイプ汚染)
  • OWASP Top 10(A03:2021 Injection)

コメント