技術部の松永です。
「サーバーサイド・テンプレート・インジェクション」という脆弱性をご存じでしょうか。
Webアプリケーションの開発やセキュリティテストに関わったことのある方でも、あまり聞き馴染みの無い脆弱性ではないかと思います。
サーバーサイド・テンプレート・インジェクション(Server-Side Template Injection:以後SSTIと表記)はサーバー内で任意の処理を実行される可能性のある非常に危険な脆弱性で、近年研究が進んでおり、PayPalやUberなど著名なサービスで実際に検出された事例があります。
弊社のWebアプリケーション診断サービスでもSSTI脆弱性を検出すべく研究を行い、オープンソースの脆弱性スキャナ開発に貢献しました。
本稿ではSSTI脆弱性の解説と、弊社の取り組みについて紹介します。
Server-Side Template Injectionとは
「テンプレートエンジン」はWikipediaで以下のように説明されています。
テンプレートエンジンはテンプレートと呼ばれる雛形と、あるデータモデルで表現される入力データを合成し、成果ドキュメントを出力するソフトウェアまたはソフトウェアコンポーネントである。テンプレートおよび成果ドキュメントは複数のこともある。成果ドキュメントには様々なフォーマットのものがあり、文書・ウェブページ・ソースコードなどがその例で、ドキュメント全体ではなく、その一部を出力することもある。
https://ja.wikipedia.org/wiki/テンプレートエンジン
Rubyのテンプレートエンジンとして使われているERBを例にして、確認してみます。
$ pry -r erb
# 静的なテンプレート文字列(eRubyスクリプト)
# - 現在時刻を考慮してご挨拶
[1] pry(main)> template = '<%= Time.now.hour<12 ? "Morning":"Hello" %>, <%= name %>.'
=> "<%= Time.now.hour<12 ? \"Morning\":\"Hello\" %>, <%= name %>."
# テンプレートにバインドするデータ
# - テンプレートのname部分の値を指定
[2] pry(main)> bind_params = binding
=> #<Binding:0x0000000314bb78>
[3] pry(main)> bind_params.local_variable_set(:name,'Takuya')
=> "Takuya"
# ERB処理結果を出力
[4] pry(main)> ERB.new(template).result(bind_params)
=> "Morning, Takuya."
テンプレートエンジンは、Webアプリケーションの世界でHTMLやメールの生成処理などに広く使われています。
しかし、使い方を誤ると重大な脆弱性を作り込んでしまうことがあります。
基本的に雛形となるテンプレートは静的にし、アプリケーションの実行中には変更せず、ユーザー名のような動的に変更したい部分にデータを流し込む形で使用されます。
テンプレートエンジンの多くはテンプレート内でのスクリプトの実行をサポートしているため、アプリケーションがテンプレートを動的に生成している場合、ユーザーが外部から指定可能な値が混入すると以下のような事象が起こりえます。
$ pry -r erb
# idコマンドを実行させる入力値
[1] pry(main)> user_input_value = '<%=`id`%>'
=> "<%=`id`%>"
# idコマンドの実行結果とテンプレートが合成された結果が表示される
[2] pry(main)> ERB.new( 'Hello,' + user_input_value + '.' ).result()
=> "Hello,uid=1000(user) gid=1000(user) groups=1000(user)\n" + "."
上記の例のようにテンプレートに不正なデータを埋め込ませることで、サーバー側で任意のテンプレート処理を実行させることが可能な脆弱性が「サーバーサイド・テンプレート・インジェクション」です。
さらに詳しい説明をお読みになりたい方は、株式会社イエラエセキュリティのブログに日本語の解説記事が公開されていますので、こちらをご覧ください。
他は全て英語になりますが、テンプレートエンジンやプログラミング言語毎の脆弱性検知/エクスプロイト手法などの記事になります。
- PortSwigger Web Security Blog: Server-Side Template Injection
- http://blog.portswigger.net/2015/08/server-side-template-injection.html
- SSTIを世に知らしめたPortSwigger社James Kettle氏のバイブル的記事
- [demo.paypal.com] Node.js code injection (RCE)
- Server Side Template Injection in Tornado
- SANDBOX BREAKOUT - A VIEW OF THE NUNJUCKS TEMPLATE ENGINE
- Exploring SSTI in Flask/Jinja2
Tplmap
オープンソースのSSTI/コードインジェクション検知/エクスプロイトツールです。
多数のテンプレートエンジンのインジェクションをサポートする他、evalのようなサーバー側のコードインジェクションもサポートしています。
ツール本体だけでなく、ツールの単体テスト環境がSSTI脆弱性の学習にとても便利なので、本稿で併せて紹介します。
ペネトレーションテスターのEmilio Pinna氏によって開発されました。
弊社は以下の機能の開発に貢献しています。
- proxyオプションのサポート
- 単体テスト環境のDockerコンテナ化
- EJSテンプレートエンジンのインジェクション
- Burp Suite Extension化
上記の機能の説明をしつつ、SSTIの理解を深めていきたいと思います。
検証環境の構築
Tplmapの単体テスト環境を使って、検証環境を構築します。
事前にDocker Compose のインストールが必要です。
ドキュメントに従って、Tplmapのdockerディレクトリで以下のコマンドを実行します。初回はDockerコンテナのビルドのため、少し時間がかかります。
$ cd /path/to/tplmap/docker
$ docker-compose up
ローカルホスト以外の端末からDockerコンテナへアクセスされたくない場合は、docker-compose.ymlファイルの各公開ポートの定義を以下のように変更してください。
ports:
- "127.0.0.1:15001:15001"
テスト用のURLがテストスクリプトに例示されています。
inj
がSSTI可能なパラメーターです。試しに、EJSテンプレートエンジンで8×9の演算を行います。
$ curl -g 'http://localhost:15004/ejs?inj=*****<%25-8*9%25>*****'
2Hg2xG4jVrUCxaHu3uAG4KPDMJu7Odr0*****72*****aIc8crx9SGWrA2RjdChYhf6DvDyH46Ps
さらに各URLにはtpl
というパラメーターがあり、こちらはサーバー側の固定文字列と仮定してテストに使うことができます。
tplパラメーターの%s
の部分が、injパラメーター値に置き換わるようになっています。
$ curl -g 'http://localhost:15004/ejs?tpl=*****Hello,<%25-"%25s"%25>-Chan*****&inj=Neko'
Q3rlhygkwzpOGQ5zBMvXK2Z10I6G7oId*****Hello,Neko-Chan*****Y9vcrB1i4K3iJHDhuc2bAwJ4GPsyWIdL
この場合はダブルクォートで囲まれているので、先程のように8×9の演算を行わせるには"+8*9+"
のように一度文字列を終了させつつ、テンプレートの構文を破壊しないようにインジェクションします。
$ curl -g 'http://localhost:15004/ejs?tpl=*****Hello,<%25-"%25s"%25>-Chan*****&inj="%2B8*9%2B"'
KSfDM35hAnNthY5kGWOCOV89c3NhCWG2*****Hello,72-Chan*****FHqEhqQgbUq6dQXwIbMH9IAN5jNTrqzi
パラメーター値が*
だとエラーを返すURLがありますので、適宜変更して使用します。
パスやパラメーターにblind
と書いてあるものは、テンプレートエンジンの出力結果をレスポンスに返しません。
ブラインドで脆弱性を検知するテクニックが必要になります。
検証環境の説明は以上です。
Tplmapを使って脆弱性をスキャンする
Tplmapを使用するには、Python2とPyYamlモジュール、requestsモジュールのインストールが必要です。
最小のオプションはURL指定です。インジェクションに成功した場合、テンプレートエンジンの情報や、この後どのようなエクスプロイトが可能かが表示されます。
$ python2 tplmap.py -u 'http://localhost:15001/reflect/jinja2?inj=test'
GET parameter: inj
Engine: Jinja2
Injection: {{*}}
Context: text
OS: posix-linux2
Technique: render
Capabilities:
Shell command execution: ok
Bind and reverse shell: ok
File write: ok
File read: ok
Code evaluation: ok, python code
エクスプロイトに関しては口を閉じるとして、検知で特に重要なものは--level
オプションです。
ヘルプにはデフォルト1と表示されますが実際にはデフォルト0、1-5までの間で数値を増やすほど検知パターンが増加します。
検証環境の構築の際、テンプレート文字列によっては"
を一度閉じる必要がある場面があることを確認しましたが、levelを増やす毎にこの閉じるパターンが増えていきます。
level0では閉じずにそのままテンプレートインジェクションを試行します。
テンプレートエンジン毎、プログラミング言語毎に閉じるパターンは異なり、安易にlevelを設定すると大量のリクエストが発生するので注意が必要です。
サーバー側で使用されているテンプレートエンジンが既知の場合、-e
オプションで指定しておくとリクエスト数を抑えることができます。
また、タイムベースのブラインド検知が必要ない場合は-t
オプションでR
を指定することで同様にリクエスト数を抑えることができます。
HTTPヘッダーに関しては、Cookie、User-Agent、任意のHTTPヘッダーの指定が可能です。
Cookieと任意のHTTPヘッダーはインジェクション対象となりますが、User-Agentは対象になりません。
HTTPヘッダーへのインジェクションを省略したい場合、インジェクション箇所を*
で明示します。
Burp SuiteなどのProxyツールを使いながら確認するとわかりやすいかと思います。
$ python2 tplmap.py -u 'http://localhost:15001/reflect/jinja2?param1=*¶m2=*' -e jinja2 -c 'cookie1=value1; cookie2=value2' -H 'X-header1: value1' -H 'X-header2: value2' -A 'nyan' --proxy localhost:8080
Burp SuiteのScanner
ScannerのActive ScanningオプションでServer-side template injection
を選択し、ActiveScanを実行することでスキャン可能です。
以下の条件で、検証環境をスキャンした様子です。
- Burp Suite Professional v1.7.23
- Scanner Options
- Scan accuracy: Normal
- Use intelligent attack: checked
- 対象URL(Scan対象のベースリクエスト/レスポンス)
- レスポンスで検知可能なもののみ(ブラインドを除外)
- tplパラメーター不指定
- injパラメーターはステータスコード200が返るよう調整
16URL中、12をServer-side template injectionとして検知しました。
PortSwigger社のブログで触れられている以下のテンプレートエンジン以外にも、ERBを検知しました。
- FreeMarker
- Velocity
- Smarty
- Twig
- Jade
- Mako
以下のテンプレートエンジンはExpression Language injectionとして検知しました。
- MarkoJS
検知されなかったのは以下の3つで、いずれもJavascriptのテンプレートエンジンでした。
- Jade(Pug)
- doT.js
- Dust.js
上記以外のテンプレートエンジンは、テンプレートエンジンが不明ではあるがSSTIとして検知するか、別のテンプレートエンジンのSSTIとして検知されました。
正直なところ、予想以上の検知結果に驚きました。もう少し複雑なテンプレートだとどうでしょうか。
検証環境のtpl
パラメーターを使って、{{ * }}
のパターン、{{ "*" }}
のパターン(*
がインジェクションポイント、{{
や}}
の部分はテンプレートエンジンによって異なる)でスキャンしてみました。
tpl
パラメーターはScanner Optionからスキャン除外設定をしています。
{{ * }}
のスキャン結果
{{ "*" }}
のスキャン結果
タグや文字列をうまく閉じることができなかったのか、検知率が低下することがわかりました。
ConfidenceもTentativeのものが多く、確証が持てないようです。
実際にはさらに複雑な構文内に脆弱性が潜んでいるケースが考えられます。
また、多くの場合問題にならないと考えられますが、Burp SuiteのActive Scanは入力値がレスポンスに反映(Reflect)されない場合、SSTIを検知することができません。
Server-Side Template Injection脆弱性の検知の難しさ
evalのようなコードインジェクションをサポートする脆弱性スキャナは数多くあります。
コードインジェクションも検知が難しい部類の脆弱性ですが、プログラミング言語毎にシンタックスを押さえればよく、ある程度似通った部分もあります。
オープンソースのスキャナでは、Burp Suite ExtensionのBackslash Powered Scanner、Burp Suite ExtensionのActiveScanPlusPlus、ZAProxyのCodeInjectionPlugin、Arachniのcode_injectionあたりが参考になるでしょうか。
SSTIはさらにテンプレートエンジン毎のシンタックスを押さえる必要がありますが、シンタックスはある程度似ていたり全く独自のものだったり様々です。
何よりテンプレートエンジンの数が多すぎるという問題があり、PortSwigger社のブログ記事Backslash Powered Scanning: Hunting Unknown Vulnerability ClassesのBlind Spot 1: Rare Technologyでも述べられていて、Wikipediaで調べただけでも100近くあるそうです(詳細は述べませんが、Backslash Powered Scanningを使えばこの問題は解決する、ということはありませんでした)。
現在のところ、Burp SuiteとTplmap以外でSSTI検知をサポートするスキャナは見つけることができませんでした。
ZAProxyにはAdd Active Scanner for Server Side Template Injectionを見ると協力者が現れたようで、実装されるかもしれません。
Tplmapは優れたSSTIスキャナですが、クローリングや他のツールとの連携機能が無く、時間の限られた脆弱性診断での実用化は難しいものがありました。
Tplmap Burp Suite Extension
この苦しい状況を少しでも打開すべく、TplmapのBurp Suite Extensionを開発しました。
インストール方法はhttps://github.com/epinna/tplmap/blob/master/burp_extension/README.md#installをご覧ください。Python製のExtensionのインストールに不慣れな方は、Burp Extension開発 - MessagePackのインストールに関する記事も併せてお読み下さい。
インストールするとTplmapというタブが追加されます。
レベル(0-5)、テクニック(Rendered/Time-based)、テンプレートエンジンの設定は、CLIのTplmapのオプションと同等です。
インジェクション箇所はScanner Optionに従う形になります。Tplmapの*
でインジェクション箇所を指定する機能はサポートされませんが、Scan manual insertion point Extensionで実現可能です。
Payload positionはExtension独自の機能で、IntruderのReplace/Appendと同等の機能になります。
Burp SuiteのScannerと同様の評価を行っていきます。
まずは以下の条件でスキャンします。
- Burp Suite Professional v1.7.23
- Scanner Options
- Active Scanning Areasのチェックを全てOFF(Burpのnativeなスキャンを実行せず、Extensionのみにする)
- 対象URL(Scan対象のベースリクエスト/レスポンス)
- レスポンスで検知可能なもののみ(ブラインドを除外)
- tplパラメーター不指定
- injパラメーターはステータスコード200が返るよう調整
- Tplmap Options
- Level: 0
- Techniques: R
- Template Engines: ALL ON
- Payload position: Replace
スキャン結果:16/16
SSTIを全て検知できました。Issueの内容を確認すると、いくつか注意点が見えてきます。
- Dust.jsのSSTIはTime-basedで検知されている
- Techniquesの設定に関わらず、Time-basedのスキャンを実行するように実装されている
- plugins/engines/dust.py
- JInja2, Tornado, Smarty, TwigがNunjucksテンプレートエンジンの脆弱性として検知される
- Nunjucksをスキャンオプションから除外することで正しく検知される
- 類似する他のテンプレートエンジンと誤判定されることがあることを理解しておく必要がある
スキャン時にどのようなHTTPメッセージが流れているのかは、FlowのようなExtensionを使うと把握しやすいと思います。
time-basdブラインドインジェクションも試してみます。
Ruby検証環境は、/blindへのリクエストが404で返る仕様になっているようですので、これを正常系としてスキャンします。
- Tplmap Options
- Level: 0
- Techniques: T
- Template Engines: ALL ON
- Payload position: Replace
スキャン結果:15/16
Twig以外の検知に成功しました。
詳細は割愛しますが、Twigはバージョン1.20.0以降でPortSwigger社のブログで公開された手法は対策されたので、これは正しい結果だと考えています。
- https://github.com/twigphp/Twig/blob/44f0f9cecdd72df00039489e162557653e03dc5d/CHANGELOG#L160
- https://github.com/twigphp/Twig/blob/44f0f9cecdd72df00039489e162557653e03dc5d/CHANGELOG#L162
テンプレートが{{ * }}
のパターン、{{ "*" }}
のパターンも確認します。
タグなどを閉じる必要がありますので、Levelは1にしました。
{{ * }}
のスキャン結果:15/16
Slimの検知に失敗しました。
実はRuby系のテンプレートはまだLevelが実装されておらず、常にLevel0の状態です。
- https://github.com/epinna/tplmap/blob/054501df3c13174908f75c5a6b98a5bd2080593a/plugins/languages/ruby.py#L74-L78
- https://github.com/epinna/tplmap/blob/054501df3c13174908f75c5a6b98a5bd2080593a/plugins/engines/slim.py#L35-L38
{{ "*" }}
のスキャン結果:10/16
Levelを2に上げるとJade(Pug)が加わり11/16になりましたが、これ以上はLevelを上げても検知率は向上しませんでした。
おわりに
Server-Side Template Injection脆弱性について解説し、弊社の取り組みを紹介いたしました。
SSTIはRCEの脅威がある非常に危険な脆弱性ですが、検出が難しく手法やツールが成熟しているとは言えない状況です。
研究を継続し、SSTIを使った攻撃が一般化する前に脆弱性を洗い出せるよう努めていきたいとおもいます。