テンプレートエンジンは、Webアプリケーションの中でHTMLとデータを組み合わせて表示を生成するために広く使われています。たとえば、ユーザー名や投稿内容などをHTMLに埋め込む場面で、バックエンドではテンプレートエンジンが活躍しています。
しかし、こうしたテンプレート処理にユーザーの入力値が直接含まれてしまうと、テンプレート構文ごと評価されてしまうことがあります。これが「SSTI(Server-Side Template Injection)」と呼ばれる脆弱性です。
SSTIが発生すると、テンプレートエンジンの種類によっては**内部の変数アクセスや、最悪の場合は任意のOSコマンドの実行(RCE)**にまで繋がる危険性があります。特にPython系では、Jinja2
や Mako
などのテンプレートエンジンに脆弱な実装があると、攻撃者にとって都合の良い「入口」になってしまいます。
今回の検証対象である Mako
は、Pythonでよく使われるテンプレートエンジンのひとつですが、構文がシンプルなぶん、誤ってユーザー入力を直接埋め込んでしまいやすいという特徴もあります。
この脆弱性が実際のアプリケーションでどのように悪用され得るのか、具体的な例を通じて見ていきます。
📹:YouTubeで実際にどのように実践しているか見ることもできます!
HackTheBoxについて
今回は、実際にHackTheBox(HTB)というプラットフォームを使って、脆弱性の検証を行っています。
HackTheBoxは、Webアプリケーションやサーバ、ネットワークなど、さまざまなセキュリティ分野の演習ができる実践型CTFプラットフォームです。
実際に攻撃対象となるマシンやアプリケーションにアクセスして、手を動かしながら学べるのが最大の特徴です。
今回取り上げる「Spookifier」は、HackTheBoxで過去に提供されていたChallengeカテゴリの1つで、現在はVIPプラン以上に加入しているユーザーのみがアクセス可能なマシンです(※無料プランではアクティブなチャレンジのみが対象になります)。
他にも、Web、Reversing、Pwn、Forensicsなど多様なカテゴリのマシンやチャレンジが揃っており、自分のレベルに合わせて取り組めるのが魅力です。
HackTheBoxで本格的にスキルを磨きたい方は、ぜひVIPプランに登録して、過去のマシンやChallengeを存分に活用してみてください。
👉 HackTheBoxの詳しい登録方法や、プランの違いはこちらをご確認ください。

チャレンジ概要:Spookifier
今回取り組んだチャレンジ「Spookifier」は、HackTheBoxのWebカテゴリに分類されている問題で、難易度はVERY EASYに設定されています。
表向きのアプリケーションは非常にシンプルで、「テキストを入力すると、異なるフォントスタイルに変換して表示してくれる」だけのWebサービスです。一見するとセキュリティ的な脆弱性があるようには見えません。
しかし、アプリケーションの内部では、Pythonのテンプレートエンジン「Mako」が使われており、ユーザーが入力した文字列がテンプレート内でそのまま評価されるという構造になっていました。
この設計ミスによって、SSTI(Server-Side Template Injection)が発生し、最終的には任意のコマンド実行(RCE)が可能になります。
👉 https://app.hackthebox.com/challenges/Spookifier
チャレンジのポイント
- 使用技術:Python, Flask, Mako
- 注目すべき挙動:入力が複数のフォントに変換されて表示される
- 攻撃ベクトル:テンプレート内での未サニタイズ入力 → SSTI
- 攻撃目標:RCEを通じて
/flag.txt
からフラグを取得
このように、表面的には安全そうに見える機能の裏にテンプレートの評価処理の甘さが潜んでおり、典型的な「SSTIからRCEへ至るパターン」を体験できる構成になっています。
実際にハッキングしてみた:SSTIからRCEへ
ここからは、実際にアプリを触ってみて、どこかに抜け道がないか探っていきます。
テンプレートの扱いが甘ければ、それがそのまま攻撃の入り口になる可能性も。
SSTIが成立するかを確かめながら、最終的にはRCEを目指します。
偵察①:まずはアプリを触ってみる
まずは対象のアプリケーションにアクセスして、どんな機能があるのか/どんな入力を受け付けているのかを確認します。
画面にはシンプルな入力フォームが1つあり、「テキストを入力すると、装飾されたフォントスタイルに変換されて出力される」だけの機能のように見えます。
見た目も構造も非常に単純で、認証機能や複雑なルーティングなどもありません。

ここで注目すべきポイントは、ユーザーが入力した文字列が何らかの処理を受けて、テンプレートとして表示されているという点です。
「複数フォントで表示する」という処理の中で、どこかにテンプレートエンジンによる文字列の操作が行われていそうです。

devtoolsでリクエスト情報を確認してみましたが、この辺にも大きな情報はなさそうです。

偵察②:ソースコードからテンプレートエンジンを特定
実際のWebアプリケーションを対象に脆弱性を調査する場合、ソースコードが手に入ることはほとんどありません。
ペネトレーションテストやBug Bountyなどでは、あくまで**ブラックボックステスト(外部からの挙動観察)**が基本になります。
そのため、通常は次のようなプロセスを通して、テンプレートエンジンの存在や脆弱性の有無を調べていきます。
- テキスト入力欄に、
${7*7}
,{{7*7}}
,<%= 7*7 %>
など、代表的なテンプレート構文をいくつか試す - 結果に
49
や7*7
のままなどが表示されるかどうかを見る - 異常なテンプレートエラーや評価結果が出れば、テンプレートエンジンの手がかりになる
- その出力や挙動から、使用されているテンプレートエンジンを推測(Mako, Jinja2, Twig, etc)
今回は特別に、脆弱性の学習を目的としたチャレンジのため、ソースコードが提供されています。
そのおかげで、内部構造を直接確認することができ、テンプレート処理がどう行われているのかをすぐに把握することができました。
提供されたソースコードの中には、以下のようなテンプレートレンダリングの記述が見つかります。
from mako.template import Template
...
return Template(result).render()
この一文から、テンプレートエンジンとしてMakoが使われていることが明確にわかります。
MakoはPython製のテンプレートエンジンで、${...}
の構文がそのままPython式として評価されるという特徴があるため、入力値の扱い次第でSSTI(Server-Side Template Injection)に繋がるリスクがあります。
スキャン・列挙:SSTIの確認⇒Makoがユーザー入力内の ${}
を評価するか試す
テンプレートエンジンに Mako が使われていることがわかったので、次に確認すべきなのは、ユーザーが入力した文字列に含まれる ${}
が実際にテンプレート式として評価されてしまうかどうかです。
これは、SSTI(Server-Side Template Injection)が成立するかを調べるための最初のステップになります。
テンプレートエンジンによって構文が異なるので、まずは Mako の基本的なテンプレート構文である ${7*7}
を入力フォームにそのまま入れてみます。
${7*7}
これは、テンプレートエンジンがこの入力を式として評価すれば、49
という数値が出力されるはずです。
逆に、評価されず文字列として扱われる場合は、${7*7}
のまま画面に表示されるだけです。
送信してみた結果、画面は次のように表示されました。

確認した結果、49
が表示されたため、ユーザーが入力した ${7*7}
が、テンプレートエンジン(Mako)によって評価されたことを意味します。
つまり、Mako が ユーザー入力をテンプレートとして処理してしまっている構造になっており、SSTI が成立していることがここで確定しました。
侵入:RCEの確認⇒テンプレート式からどこまで実行できるか試す
SSTIが確認できた時点で、次の目標は明確です。
テンプレート式を利用して、サーバー側で任意のコードを実行できるか(RCE)を調べていきます。
コマンド実行の確認
まずは、テンプレート式から実際にOSコマンドが実行できるかを試してみます。
確認に使うのは、シンプルな whoami
コマンドです。
${__import__('os').popen('whoami').read()}
この式を送信すると、次のような結果が返ってきました。

whoami
の実行結果として root
が表示されていることから、テンプレート式を通じて任意のOSコマンドが実行可能であること、さらにそのコマンドがroot権限で実行されていることが確認できました。
この時点で、リモートコード実行(RCE)の脆弱性が存在しており、攻撃者が高権限でコマンドを実行できる状態であることが明らかです。
ファイル読み取りの確認
コマンド実行が可能であることがわかったので、次は任意のファイルを読み取れるかどうかを確認してみます。
典型的な確認対象は Linux 環境でおなじみの /etc/passwd
ファイルです。
${open('/etc/passwd').read()}
出力には、次のようなユーザー情報が含まれていました。

この結果から、テンプレート式を通じて Python の open()
関数を呼び出し、任意のファイルを読み取ることが可能であることがわかります。
つまり、サーバー上のファイルに対する読み取り操作も完全にコントロールできる状態です。
実行:フラグの探索と取得
ここまでで、テンプレートインジェクションを通じて
- Pythonコードの実行
- OSコマンドの実行
- 任意ファイルの読み取り
が可能であることが確認できました。
つまり、完全なリモートコード実行(RCE)が成立しています。
次はいよいよ、目的となるフラグファイルを見つけ出し、その中身を取得するフェーズです。
フラグファイルの場所を探す
まずは、ルートディレクトリ /
の中身を見て、どんなディレクトリやファイルがあるかを調べてみます。
${__import__('os').listdir('/')}

このように、目的のファイル(flag.txt
)が見つかればよいですが、もし見つからない場合は home
や root
の中をさらに深掘りしていく必要があります。
フラグの読み取り
探索の結果、/flag.txt
のようなファイルが見つかったら、以下のように open()
を使って中身を取得します。
${open('/flag.txt').read()}
この式を実行すると、次のような出力が得られました。

無事、HackTheBoxチャレンジにおける最終目的であるフラグの取得に成功しました。
ここまでの流れで、SSTI脆弱性がどこまで悪用可能かを、実際に試しながら深く理解することができました。
なぜこの脆弱性が生まれたのか?安全なテンプレートの使い方
今回の脆弱性(SSTI → RCE)は、テンプレートエンジンの危険な使い方が主な原因でした。
特に、ユーザー入力を含むテンプレート文字列をPython側で動的に構築していたことが、テンプレートインジェクション(SSTI)と最終的なリモートコード実行(RCE)につながっています。
対策 – テンプレートに動的な文字列を渡さない
SSTI(Server-Side Template Injection)が発生する典型的なパターンの一つが、
Pythonで文字列を組み立てたあと、それをテンプレートとして評価させる方法です。
以下のコードのように、str.format()
を使ってテンプレート文字列を作るのは非常に危険です。
def generate_render(converted_fonts):
result = '''
<tr>
<td>{0}</td>
</tr>
<tr>
<td>{1}</td>
</tr>
<tr>
<td>{2}</td>
</tr>
<tr>
<td>{3}</td>
</tr>
'''.format(*converted_fonts)
return Template(result).render()
たとえば、converted_fonts
の中に以下のようなユーザー入力が含まれていた場合:
${__import__('os').popen('id').read()}
このテンプレートを Mako に渡すことで、Makoが ${...}
をテンプレート構文として評価してしまい、OSコマンドが実行されるという深刻な脆弱性が発生します。
安全な使い方:テンプレート構造とデータを分離する
このような問題を防ぐには、テンプレート構造とユーザー入力(変数)を分離することが重要です。
以下のように、変数を render()
に明示的に渡す方法で安全に処理できます。
def generate_render(converted_fonts):
template = '''
<tr><td>${font1}</td></tr>
<tr><td>${font2}</td></tr>
<tr><td>${font3}</td></tr>
<tr><td>${font4}</td></tr>
'''
return Template(template).render(
font1=converted_fonts[0],
font2=converted_fonts[1],
font3=converted_fonts[2],
font4=converted_fonts[3]
)
この方法であれば、font1
〜font4
の中に ${}
が含まれていても、Makoはそれをただの文字列として扱うため、テンプレート式として評価されることはありません。

Makoの ${...}
はテンプレート内で式を評価する構文です。
そこにユーザー入力がそのまま入り込むと、コードが実行されてしまうリスクがあります。
一方で、Template(...).render(var=value)
のように変数を明示的に渡す方法なら、Makoは中身を単なる文字列として処理し、意図しない評価を行いません。
この場合、${var}
はテンプレート内のプレースホルダとして安全に使われるだけで、コードとして実行されることはありません。
まとめ:SSTIは「使い方のクセ」が招く脆弱性
今回の「Spookifier」チャレンジでは、Makoテンプレートを不適切に扱ったことで、
SSTI(Server-Side Template Injection)から最終的にRCE(リモートコード実行)に至る脆弱性を突くことができました。
このような脆弱性は、テンプレートエンジンそのものの問題というよりも、開発者の実装ミスによって生まれます。
特に、テンプレート文字列を動的に構築するようなケースは非常に危険です。
テンプレートエンジンを安全に使うためには:
- テンプレートとデータ(変数)は必ず分離する
- プレースホルダとして変数を渡し、式評価させない
- 信頼できない入力はそのままテンプレートに渡さない
この基本を守るだけで、SSTIの多くは防げます。
👉 HackTheBoxの詳しい登録方法や、プランの違いはこちらをご確認ください。
